diff --git a/CLAUDE.md b/CLAUDE.md index ac2e600..c26bad0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -217,6 +217,7 @@ Redis with 15-min TTL for rate quotes. Key format: `rate:{origin}:{destination}: - RBAC Roles: ADMIN, MANAGER, USER, VIEWER, CARRIER - JWT: access token 15min, refresh token 7d - Password hashing: Argon2 +- OAuth providers: Google, Microsoft (configured via passport strategies) ### Carrier Portal Workflow 1. Admin creates CSV booking → assigns carrier @@ -239,14 +240,15 @@ Redis with 15-min TTL for rate quotes. Key format: `rate:{origin}:{destination}: 1. **Domain Entity** → `domain/entities/*.entity.ts` (pure TS, unit tests) 2. **Value Objects** → `domain/value-objects/*.vo.ts` (immutable) -3. **Port Interface** → `domain/ports/out/*.repository.ts` (with token constant) -4. **ORM Entity** → `infrastructure/persistence/typeorm/entities/*.orm-entity.ts` -5. **Migration** → `npm run migration:generate -- src/infrastructure/persistence/typeorm/migrations/MigrationName` -6. **Repository Impl** → `infrastructure/persistence/typeorm/repositories/` -7. **Mapper** → `infrastructure/persistence/typeorm/mappers/` (static toOrm/toDomain/toDomainMany) -8. **DTOs** → `application/dto/` (with class-validator decorators) -9. **Controller** → `application/controllers/` (with Swagger decorators) -10. **Module** → Register and import in `app.module.ts` +3. **In Port (Use Case)** → `domain/ports/in/*.use-case.ts` (interface with `execute()`) +4. **Out Port (Repository)** → `domain/ports/out/*.repository.ts` (with token constant) +5. **ORM Entity** → `infrastructure/persistence/typeorm/entities/*.orm-entity.ts` +6. **Migration** → `npm run migration:generate -- src/infrastructure/persistence/typeorm/migrations/MigrationName` +7. **Repository Impl** → `infrastructure/persistence/typeorm/repositories/` +8. **Mapper** → `infrastructure/persistence/typeorm/mappers/` (static toOrm/toDomain/toDomainMany) +9. **DTOs** → `application/dto/` (with class-validator decorators) +10. **Controller** → `application/controllers/` (with Swagger decorators) +11. **Module** → Register repository + use-case providers, import in `app.module.ts` ## Documentation @@ -254,3 +256,5 @@ Redis with 15-min TTL for rate quotes. Key format: `rate:{origin}:{destination}: - Setup guide: `docs/installation/START-HERE.md` - Carrier Portal API: `apps/backend/docs/CARRIER_PORTAL_API.md` - Full docs index: `docs/README.md` +- Development roadmap: `TODO.md` +- Infrastructure configs (CI/CD, Docker): `infra/` diff --git a/docs/deployment/AWS_COSTS_KUBERNETES.md b/docs/deployment/AWS_COSTS_KUBERNETES.md new file mode 100644 index 0000000..c30cf5e --- /dev/null +++ b/docs/deployment/AWS_COSTS_KUBERNETES.md @@ -0,0 +1,565 @@ +# Estimation des Coûts AWS — Déploiement Production Kubernetes (EKS) + +> Document de référence — Xpeditis 2.0 +> Région cible : `us-east-1` (ou `eu-west-1` pour conformité RGPD) +> Base tarifaire : AWS on-demand, mars 2026 +> MinIO remplacé par S3 + +--- + +## Table des matières + +1. [Architecture cible sur EKS](#1-architecture-cible-sur-eks) +2. [Inventaire des composants analysés](#2-inventaire-des-composants-analysés) +3. [Hypothèses par palier d'utilisateurs](#3-hypothèses-par-palier-dutilisateurs) +4. [Détail des coûts — 100 utilisateurs](#4-détail-des-coûts--100-utilisateurs) +5. [Détail des coûts — 1 000 utilisateurs](#5-détail-des-coûts--1-000-utilisateurs) +6. [Détail des coûts — 10 000 utilisateurs](#6-détail-des-coûts--10-000-utilisateurs) +7. [Tableau récapitulatif](#7-tableau-récapitulatif) +8. [Économies avec Reserved Instances](#8-économies-avec-reserved-instances) +9. [Facteurs de risque et dépassements potentiels](#9-facteurs-de-risque-et-dépassements-potentiels) +10. [Recommandations d'optimisation](#10-recommandations-doptimisation) +11. [Architecture Kubernetes recommandée](#11-architecture-kubernetes-recommandée) + +--- + +## 1. Architecture cible sur EKS + +``` + ┌─────────────────────────────────────────────┐ + │ AWS VPC (Multi-AZ) │ + │ │ + Internet ──── Route53 ──┤── CloudFront ──── ALB ──┬── NestJS Pods │ + │ │ (EKS NodeGroup) │ + │ └── Next.js Pods │ + │ │ + │ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ + │ │ RDS PG15 │ │ElastiCache│ │ S3 │ │ + │ │ Multi-AZ │ │ Redis7 │ │ Bucket │ │ + │ └──────────┘ └──────────┘ └─────────┘ │ + │ │ + │ ┌──────────┐ ┌──────────┐ │ + │ │ SES │ │Secrets │ │ + │ │ Email │ │Manager │ │ + │ └──────────┘ └──────────┘ │ + └─────────────────────────────────────────────┘ +``` + +**Services AWS utilisés :** +| Service AWS | Remplace / Rôle | +|---|---| +| **EKS** | Orchestration des containers (backend NestJS + frontend Next.js) | +| **RDS PostgreSQL 15** | Base de données principale (18 tables, 29 migrations) | +| **ElastiCache Redis 7** | Cache des rate quotes (TTL 15 min), pub/sub WebSocket | +| **S3** | Remplace MinIO — PDFs, documents carrier, exports CSV/Excel | +| **ALB** | Load balancer HTTPS + WebSocket (sticky sessions) | +| **CloudFront** | CDN pour assets frontend et PDFs publics | +| **SES** | 10 types d'emails transactionnels (confirmations, invitations, magic links) | +| **Secrets Manager** | JWT secret, DB passwords, API keys carriers (5 carriers) | +| **Route 53** | DNS | +| **CloudWatch** | Logs (nestjs-pino), métriques, alertes | +| **WAF** | Protection OWASP (Rate limiting déjà implémenté dans NestJS) | +| **NAT Gateway** | Accès internet pour les pods (appels APIs carriers) | + +--- + +## 2. Inventaire des composants analysés + +### Backend NestJS (charges identifiées) + +**Endpoints critiques en charge :** +- `POST /api/v1/rates/search` — appelle **5 carriers APIs en parallèle** (Maersk, MSC, CMA CGM, Hapag-Lloyd, ONE) avec circuit breaker 5s timeout. C'est le endpoint le plus coûteux en compute. +- `GET /api/v1/rates/csv-search` — filtrage in-memory de fichiers CSV uploadés +- WebSocket `/notifications` — connexions persistantes Socket.IO, pub/sub via Redis + +**Redis (ElastiCache) :** +- Clés : `rate:{origin}:{destination}:{containerType}` — TTL 15 minutes +- Taux de cache hit estimé : 70–90% pendant les heures de bureau +- Pas de cluster mode nécessaire avant ~500 utilisateurs actifs simultanés + +**Base de données (RDS) :** +- 18 tables avec indexes composites +- Tables à forte croissance : `bookings`, `audit_logs`, `notifications`, `rate_quotes` +- `audit_logs` peut grossir très vite (~50–200K lignes/mois à 1 000 users) +- Extensions PostgreSQL : `pg_trgm` (recherche textuelle sur ports) + +**S3 (remplace MinIO) :** +- PDFs booking : 100–500 KB/document, 1 par booking + exports +- Documents carrier : jusqu'à 10 MB/fichier (PDF, images, Excel, CSV) +- Logos d'organisations +- Exports CSV/Excel des bookings + +**Emails (SES) :** +1. Vérification email à l'inscription +2. Mot de passe oublié / reset +3. Email de bienvenue +4. Invitation d'utilisateur (token 1h) +5. Confirmation de booking avec PDF en pièce jointe +6. Demande CSV booking au carrier (magic link) +7. Création compte carrier +8. Reset password carrier +9. Notification nouveaux documents +10. Alerte accès documents + +**Appels APIs externes :** +- 5 carriers APIs consultées à chaque rate search non mis en cache +- Latence estimée : 1–3 secondes par carrier +- Volume estimé : 10–30% des searches frappent les APIs (le reste vient du cache Redis) + +### Frontend Next.js (charges identifiées) + +- Mode `standalone` — container Docker autonome avec Node.js +- **SSR / SSG hybride** — pages dashboard en CSR, landing page statique +- Bundle gzippé estimé : 200–400 KB (React 19 + Shadcn/ui + TanStack Query) +- Leaflet (cartes), Recharts (graphiques), Framer Motion (animations) +- Images optimisées via `next/image` avec support S3 (`**.amazonaws.com`) + +--- + +## 3. Hypothèses par palier d'utilisateurs + +| Paramètre | 100 users | 1 000 users | 10 000 users | +|---|---|---|---| +| Utilisateurs actifs simultanés (pic) | 10–20 | 100–300 | 1 000–3 000 | +| Rate searches / jour | 200–500 | 2 000–8 000 | 20 000–80 000 | +| Bookings créés / mois | 20–50 | 300–800 | 3 000–8 000 | +| Connexions WebSocket simultanées | 10–20 | 100–300 | 1 000–3 000 | +| Emails / mois | 200–500 | 5 000–20 000 | 50 000–200 000 | +| Volume S3 total | 5–20 GB | 100–300 GB | 1–3 TB | +| Upload S3 / mois | 1–5 GB | 20–50 GB | 200–500 GB | +| Trafic sortant APIs carriers / mois | 5–20 GB | 100–300 GB | 1–3 TB | +| Lignes audit_logs cumulées | < 50K | < 1M | < 20M | + +--- + +## 4. Détail des coûts — 100 utilisateurs + +> Phase early-stage / MVP. Architecture sans Multi-AZ sur certains composants pour réduire les coûts. + +### Compute — EKS + +| Ressource | Config | Coût/mois | +|---|---|---| +| EKS Control Plane | 1 cluster | **$73** | +| Worker nodes | 2× `t3.medium` (2 vCPU, 4 GB) — backend + frontend | **$61** | +| **Total compute** | | **$134** | + +> Pods : 2 replicas NestJS (500m CPU / 512Mi chacun) + 1 replica Next.js (250m CPU / 256Mi). HPA désactivé à ce stade. + +### Base de données — RDS PostgreSQL + +| Ressource | Config | Coût/mois | +|---|---|---| +| Instance | `db.t4g.medium` (2 vCPU, 4 GB) — Single-AZ | **$60** | +| Stockage | 20 GB gp3 | **$2** | +| Backups | 7 jours retention | **$2** | +| **Total RDS** | | **$64** | + +### Cache — ElastiCache Redis + +| Ressource | Config | Coût/mois | +|---|---|---| +| Instance | `cache.t4g.micro` (1 nœud, 0.5 GB) | **$12** | + +> Suffisant pour ~10 000 clés rate quotes en cache. Pas de réplication à ce stade. + +### Stockage — S3 + +| Ressource | Volume | Coût/mois | +|---|---|---| +| Stockage standard | 10 GB | **$0.23** | +| Requêtes PUT/GET | ~50 000/mois | **$0.50** | +| Transfert sortant | 5 GB | **$0.45** | +| **Total S3** | | **~$2** | + +### Réseau + +| Ressource | Config | Coût/mois | +|---|---|---| +| ALB | 1 load balancer, ~5 LCU | **$22** | +| NAT Gateway | 1 NAT, ~20 GB data processing | **$34** | +| CloudFront | Distribution basique, ~5 GB transfert | **$5** | +| **Total réseau** | | **$61** | + +### Services managés + +| Ressource | Usage | Coût/mois | +|---|---|---| +| SES | ~500 emails (dont ~50 avec PDF joint ~250 KB) | **$1** | +| Secrets Manager | 12 secrets | **$5** | +| Route 53 | 1 hosted zone + queries | **$2** | +| CloudWatch | Logs 5 GB/mois + métriques de base | **$15** | +| **Total services** | | **$23** | + +### Total Tier 1 — 100 utilisateurs + +| Catégorie | Coût/mois | +|---|---| +| Compute (EKS) | $134 | +| RDS PostgreSQL | $64 | +| ElastiCache Redis | $12 | +| S3 | $2 | +| Réseau (ALB + NAT + CloudFront) | $61 | +| Services (SES + Secrets + R53 + CloudWatch) | $23 | +| **TOTAL** | **~$296/mois** | +| **Avec buffer 15%** | **~$340/mois** | + +--- + +## 5. Détail des coûts — 1 000 utilisateurs + +> Phase croissance. Multi-AZ activé sur RDS, Redis répliqué, autoscaling EKS. + +### Compute — EKS + +| Ressource | Config | Coût/mois | +|---|---|---| +| EKS Control Plane | 1 cluster | **$73** | +| Worker nodes | 3× `t3.xlarge` (4 vCPU, 16 GB) — autoscaling 3–6 nodes | **$362** | +| **Total compute** | | **$435** | + +> Pods : 4 replicas NestJS (1 CPU / 1 GB chacun) + 2 replicas Next.js. HPA activé (CPU > 70%). + +### Base de données — RDS PostgreSQL + +| Ressource | Config | Coût/mois | +|---|---|---| +| Instance | `db.r6g.large` (2 vCPU, 16 GB) — **Multi-AZ** | **$350** | +| Stockage | 100 GB gp3 | **$10** | +| RDS Proxy | Connection pooling (critique pour NestJS) | **$36** | +| Backups | 30 jours retention | **$20** | +| **Total RDS** | | **$416** | + +> RDS Proxy devient essentiel à partir de ~50 connexions concurrentes pour éviter les `too many connections`. + +### Cache — ElastiCache Redis + +| Ressource | Config | Coût/mois | +|---|---|---| +| Instance | `cache.r6g.large` (2 nœuds, 13 GB) — réplication activée | **$242** | + +> La réplication est nécessaire pour le pub/sub WebSocket multi-pods (Socket.IO scale-out). + +### Stockage — S3 + +| Ressource | Volume | Coût/mois | +|---|---|---| +| Stockage standard | 150 GB | **$3.45** | +| Stockage Intelligent-Tiering (archives) | 50 GB | **$2.50** | +| Requêtes PUT/GET | ~500 000/mois | **$3** | +| Transfert sortant | 50 GB | **$4.50** | +| **Total S3** | | **~$13** | + +### Réseau + +| Ressource | Config | Coût/mois | +|---|---|---| +| ALB | 1 load balancer, ~50 LCU | **$38** | +| NAT Gateway | 2 NATs (HA Multi-AZ), ~150 GB data | **$73** | +| CloudFront | ~50 GB transfert (assets + PDFs) | **$25** | +| **Total réseau** | | **$136** | + +### Services managés + +| Ressource | Usage | Coût/mois | +|---|---|---| +| SES | ~15 000 emails/mois | **$1.50** | +| SES — pièces jointes PDF | ~300 bookings × 250 KB | **$0.10** | +| Secrets Manager | 15 secrets | **$6** | +| Route 53 | 1 zone + queries | **$3** | +| CloudWatch | Logs 20 GB/mois + métriques + dashboards | **$50** | +| WAF | 1 WebACL, 5 rules, ~5M requêtes/mois | **$12** | +| **Total services** | | **$73** | + +### Total Tier 2 — 1 000 utilisateurs + +| Catégorie | Coût/mois | +|---|---| +| Compute (EKS) | $435 | +| RDS PostgreSQL (Multi-AZ + Proxy) | $416 | +| ElastiCache Redis (répliqué) | $242 | +| S3 | $13 | +| Réseau (ALB + NAT + CloudFront) | $136 | +| Services (SES + Secrets + R53 + CW + WAF) | $73 | +| **TOTAL** | **~$1 315/mois** | +| **Avec buffer 15%** | **~$1 512/mois** | + +--- + +## 6. Détail des coûts — 10 000 utilisateurs + +> Phase scale. Read replica RDS, Redis cluster mode, autoscaling agressif, WAF renforcé. + +### Compute — EKS + +| Ressource | Config | Coût/mois | +|---|---|---| +| EKS Control Plane | 1 cluster | **$73** | +| Worker nodes — backend | 4× `m6i.xlarge` (4 vCPU, 16 GB) — autoscaling 4–12 | **$560** | +| Worker nodes — frontend | 2× `m6i.large` (2 vCPU, 8 GB) — autoscaling 2–6 | **$140** | +| **Total compute** | | **$773** | + +> Pods NestJS : 8–15 replicas (1.5 CPU / 1.5 GB chacun). HPA + KEDA si adoption de SQS. +> Pods Next.js : 4–8 replicas (500m CPU / 512 Mi chacun). + +### Base de données — RDS PostgreSQL + +| Ressource | Config | Coût/mois | +|---|---|---| +| Instance primaire | `db.r6g.2xlarge` (8 vCPU, 64 GB) — **Multi-AZ** | **$1 402** | +| Read Replica | `db.r6g.xlarge` (analytics + audit queries) | **$350** | +| Stockage | 1 TB gp3 (SSD) | **$100** | +| RDS Proxy | Haute disponibilité | **$100** | +| Backups | 30 jours + point-in-time recovery | **$100** | +| **Total RDS** | | **$2 052** | + +> À 10 000 users, `audit_logs` peut dépasser 10M lignes. Envisager un archivage vers S3/Athena après 90 jours. + +### Cache — ElastiCache Redis + +| Ressource | Config | Coût/mois | +|---|---|---| +| Cluster mode | 3 shards × 2 nœuds `cache.r6g.large` (13 GB/shard) | **$1 452** | + +> Cluster mode nécessaire pour distribuer les ~100 000+ clés rate quotes et supporter le pub/sub à grande échelle. + +### Stockage — S3 + +| Ressource | Volume | Coût/mois | +|---|---|---| +| Stockage Standard | 500 GB (documents actifs < 30 jours) | **$11.50** | +| Stockage Intelligent-Tiering | 1 TB (documents anciens) | **$23** | +| Glacier Instant Retrieval | 2 TB (archives > 6 mois) | **$8** | +| Requêtes PUT/GET | ~5M/mois | **$20** | +| Transfert sortant S3 | 200 GB (via CloudFront réduit les coûts) | **$9** | +| **Total S3** | | **~$72** | + +### Réseau + +| Ressource | Config | Coût/mois | +|---|---|---| +| ALB | 2 load balancers (backend + frontend), ~200 LCU | **$110** | +| NAT Gateway | 2 NATs, ~1 TB appels carriers + Redis | **$111** | +| CloudFront | ~500 GB transfert (assets + PDFs + exports) | **$125** | +| **Total réseau** | | **$346** | + +### Services managés + +| Ressource | Usage | Coût/mois | +|---|---|---| +| SES | ~120 000 emails/mois | **$12** | +| SES — pièces jointes PDF | ~5 000 bookings × 300 KB | **$1.80** | +| Secrets Manager | 20 secrets + 2M API calls/mois | **$18** | +| Route 53 | 2 zones (prod + staging), traffic routing | **$10** | +| CloudWatch | Logs 100 GB/mois + métriques + Container Insights | **$200** | +| WAF + Shield Standard | WebACL, 10 rules, ~50M requêtes/mois, DDoS basique | **$65** | +| KMS | Encryption at rest RDS/S3, rotation clés | **$10** | +| **Total services** | | **$317** | + +### Total Tier 3 — 10 000 utilisateurs + +| Catégorie | Coût/mois | +|---|---| +| Compute (EKS) | $773 | +| RDS PostgreSQL (Multi-AZ + Replica + Proxy) | $2 052 | +| ElastiCache Redis (cluster mode) | $1 452 | +| S3 (avec tiering) | $72 | +| Réseau (ALB + NAT + CloudFront) | $346 | +| Services (SES + Secrets + R53 + CW + WAF + KMS) | $317 | +| **TOTAL** | **~$5 012/mois** | +| **Avec buffer 15%** | **~$5 764/mois** | + +--- + +## 7. Tableau récapitulatif + +| Composant | 100 users | 1 000 users | 10 000 users | +|---|---|---|---| +| EKS (Control Plane + Nodes) | $134 | $435 | $773 | +| RDS PostgreSQL | $64 | $416 | $2 052 | +| ElastiCache Redis | $12 | $242 | $1 452 | +| S3 | $2 | $13 | $72 | +| Réseau (ALB + NAT + CDN) | $61 | $136 | $346 | +| Services managés | $23 | $73 | $317 | +| **TOTAL (on-demand)** | **$296** | **$1 315** | **$5 012** | +| **TOTAL avec buffer 15%** | **~$340** | **~$1 512** | **~$5 764** | + +> **Note hors scope :** Stripe facture 2.9% + $0.30 par transaction. Pour 500 transactions/mois à $500 chacune, cela représente ~$7 400/mois de frais Stripe — à prendre en compte dans la marge business, pas dans l'infra. + +--- + +## 8. Économies avec Reserved Instances + +Engager 1 ou 3 ans sur les ressources stables (RDS, ElastiCache, nœuds EKS de base) permet des économies significatives. + +### 1 an — Savings Plans + +| Composant | Coût on-demand | Après 1 an RI (~35% réduction) | +|---|---|---| +| RDS (1 000 users) | $416 | **~$270** | +| ElastiCache (1 000 users) | $242 | **~$157** | +| EC2 workers EKS (1 000 users) | $362 | **~$235** | +| **Économie mensuelle** | | **~$358/mois** | + +### Impact par palier avec Reserved Instances 1 an + +| Palier | On-demand | Avec RI 1 an | Économie annuelle | +|---|---|---|---| +| 100 users | $296 | ~$220 | **~$912** | +| 1 000 users | $1 315 | ~$950 | **~$4 380** | +| 10 000 users | $5 012 | ~$3 600 | **~$16 944** | + +--- + +## 9. Facteurs de risque et dépassements potentiels + +### 🔴 Risque élevé — Appels APIs carriers + +Chaque `POST /rates/search` sans cache Redis déclenche **5 appels HTTP externes en parallèle**. +À 1 000 users avec 30% de cache miss → ~2 400 appels/heure vers les carriers. +**Impact NAT Gateway :** transfert data $0.045/GB. Si chaque réponse carrier fait 10 KB → ~100 GB/mois → $4.50. +Si les réponses sont plus volumineuses (50 KB+) ou si le cache miss monte → coût × 5 à × 10. + +**Mitigation :** Ajuster le TTL Redis (actuellement 15 min) à 30–60 min pour les routes peu demandées. + +### 🔴 Risque élevé — audit_logs à 10 000 users + +À 10 000 users, `audit_logs` peut atteindre **50–200M lignes/an** (toutes actions loggées). +RDS `db.r6g.2xlarge` avec 1 TB de stockage sera rapidement saturé. +**Mitigation :** Mettre en place un archivage automatique des logs > 90 jours vers S3 + Athena pour les requêtes analytiques. + +### 🟡 Risque moyen — WebSocket à grande échelle + +Socket.IO avec Redis adapter fonctionne bien jusqu'à ~2 000 connexions simultanées sur un cache.r6g.large. +Au-delà, envisager de passer à **Redis Cluster mode** ou à une gateway WebSocket dédiée. + +### 🟡 Risque moyen — PDFs avec pièces jointes emails (SES) + +Les booking confirmations envoient le PDF en **base64 dans le corps de l'email** (détecté dans le code email service). +Pour 5 000 bookings/mois à 300 KB/PDF → **1.5 GB de data SES/mois** → $0.18/mois en plus des emails. +À grande échelle, préférer un **lien S3 signé** dans l'email plutôt qu'une pièce jointe. + +### 🟡 Risque moyen — ElastiCache sous-dimensionné + +Le nombre de clés Redis peut exploser si les recherches sont très diversifiées (nombreuses combinaisons origin/destination/containerType). +Monitorer `cache.CurrItems` et `BytesUsedForCache` dans CloudWatch dès le démarrage. + +### 🟢 Risque faible — S3 + +Les coûts S3 restent maîtrisés avec les politiques de cycle de vie (Intelligent-Tiering + Glacier). +La migration MinIO → S3 est transparente (l'app utilise déjà le SDK AWS S3 v3). + +--- + +## 10. Recommandations d'optimisation + +### Priorité haute (impact fort, faible effort) + +1. **S3 Lifecycle Policies dès le J1** + - Documents > 30 jours → Intelligent-Tiering + - Documents > 180 jours → Glacier Instant Retrieval + - Économie estimée : 50–70% sur les coûts de stockage long terme + +2. **RDS Proxy activé dès 1 000 users** + - NestJS ouvre N connexions par pod × N pods → sans proxy, RDS sature + - `db.r6g.large` supporte ~150 connexions max, 10 pods × 10 connexions = limite atteinte + +3. **CloudFront pour tous les assets S3 publics** + - Élimine le transfert sortant S3 (×8 fois moins cher via CloudFront) + - Mettre en cache les PDFs de booking (signé URL → CloudFront signed URL) + +4. **Reserved Instances 1 an sur RDS + ElastiCache** + - Ces deux services représentent 50–60% de la facture à 1 000+ users + - Le ROI est atteint dès le premier mois comparé à l'on-demand + +### Priorité moyenne + +5. **Archivage audit_logs vers S3 + Athena** + - Créer un job cron mensuel qui exporte les logs > 90 jours en Parquet vers S3 + - Requêtes analytiques via Athena ($5 par TB scanné) + - Libère l'espace RDS et maintient les performances des indexes + +6. **SQS + Lambda pour la génération de PDFs et l'envoi d'emails** + - Actuellement synchrone → bloque le thread NestJS + - Découpler : POST /bookings → enfile en SQS → Lambda génère PDF + envoie email + - Réduit les besoins CPU des pods NestJS (~20% de réduction possible) + +7. **Lien S3 signé dans les emails plutôt que pièce jointe** + - Remplacer le PDF base64 dans SES par un lien CloudFront signé (1h expiry) + - Réduit la taille des emails de 60–70% et évite les filtres anti-spam + +### Priorité basse + +8. **Fargate Spot pour les pods non-critiques** + - Workers de génération PDF/export peuvent tourner sur Fargate Spot (70% moins cher) + +9. **KEDA (Kubernetes Event-Driven Autoscaling)** + - Scaler les pods NestJS selon la profondeur de la file SQS plutôt que le CPU + +--- + +## 11. Architecture Kubernetes recommandée + +### Namespaces + +``` +xpeditis-prod +├── backend # NestJS pods +├── frontend # Next.js pods +└── monitoring # Prometheus + Grafana (optionnel) +``` + +### Sizing des pods par palier + +#### Backend NestJS + +| Palier | Replicas (base) | Replicas (max HPA) | CPU request/limit | RAM request/limit | +|---|---|---|---|---| +| 100 users | 2 | 3 | 500m / 1500m | 512Mi / 1Gi | +| 1 000 users | 4 | 8 | 750m / 2000m | 768Mi / 1.5Gi | +| 10 000 users | 8 | 20 | 1000m / 3000m | 1Gi / 2Gi | + +#### Frontend Next.js (standalone) + +| Palier | Replicas (base) | Replicas (max HPA) | CPU request/limit | RAM request/limit | +|---|---|---|---|---| +| 100 users | 1 | 2 | 250m / 1000m | 256Mi / 512Mi | +| 1 000 users | 2 | 4 | 500m / 1500m | 512Mi / 1Gi | +| 10 000 users | 3 | 8 | 750m / 2000m | 512Mi / 1Gi | + +### Variables d'environnement Kubernetes (Secrets) + +À stocker dans **AWS Secrets Manager** et monter via External Secrets Operator : +- `DATABASE_URL` → RDS connection string avec RDS Proxy endpoint +- `REDIS_HOST` / `REDIS_PASSWORD` → ElastiCache primary endpoint +- `JWT_SECRET` → rotation automatique mensuelle recommandée +- `AWS_S3_BUCKET` → remplace `MINIO_ENDPOINT` / `MINIO_ACCESS_KEY` / `MINIO_SECRET_KEY` +- `STRIPE_SECRET_KEY` / `STRIPE_WEBHOOK_SECRET` +- API keys × 5 carriers (Maersk, MSC, CMA CGM, Hapag-Lloyd, ONE) +- `SMTP_HOST` → SES SMTP endpoint + credentials + +### Ingress (ALB Ingress Controller) + +```yaml +# Règles d'ingress recommandées +/api/* → backend service (port 4000) +/socket.io/* → backend service (sticky sessions activées) +/* → frontend service (port 3000) +``` + +> Le WebSocket Socket.IO nécessite `alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true` + +--- + +## Résumé exécutif + +| | 100 users | 1 000 users | 10 000 users | +|---|---|---|---| +| **Coût mensuel estimé** | **~$340** | **~$1 500** | **~$5 800** | +| **Coût annuel** | **~$4 080** | **~$18 000** | **~$69 600** | +| **Avec RI 1 an** | **~$2 640** | **~$11 400** | **~$43 200** | +| **Poste dominant** | Compute + Réseau | RDS Multi-AZ | RDS + Redis | +| **Principal risque** | Sur-dimensionnement | Connexions DB | audit_logs volume | + +> Les prix sont indicatifs en us-east-1 (on-demand, mars 2026). `eu-west-1` (Paris : `eu-west-3`) est ~5–10% plus cher. +> Ces estimations **excluent** : les frais Stripe (2.9% + $0.30/transaction), les licences SaaS tierces éventuelles, et les coûts de développement/opération. diff --git a/docs/deployment/CLOUD_COST_COMPARISON.md b/docs/deployment/CLOUD_COST_COMPARISON.md new file mode 100644 index 0000000..4e21dc6 --- /dev/null +++ b/docs/deployment/CLOUD_COST_COMPARISON.md @@ -0,0 +1,548 @@ +# Comparatif Cloud — Coûts de Production Xpeditis 2.0 +## Aide à la décision : AWS vs GCP vs Azure vs Hetzner vs OVHcloud vs DigitalOcean vs Scaleway + +> Analyse réalisée le 23 mars 2026 — prix on-demand vérifiés sur les sites officiels +> Scénarios : 100 / 1 000 / 10 000 utilisateurs sur Kubernetes +> MinIO → stockage objet S3-compatible de chaque fournisseur +> ⚠️ **Hetzner augmente ses prix de ~35% le 1er avril 2026** — prix actuels ET futurs indiqués + +--- + +## Table des matières + +1. [Ce que l'app exige comme infrastructure](#1-ce-que-lapp-exige-comme-infrastructure) +2. [Vue d'ensemble des fournisseurs](#2-vue-densemble-des-fournisseurs) +3. [Tableau comparatif — 100 utilisateurs](#3-tableau-comparatif--100-utilisateurs) +4. [Tableau comparatif — 1 000 utilisateurs](#4-tableau-comparatif--1-000-utilisateurs) +5. [Tableau comparatif — 10 000 utilisateurs](#5-tableau-comparatif--10-000-utilisateurs) +6. [Récapitulatif global](#6-récapitulatif-global) +7. [Analyse détaillée par fournisseur](#7-analyse-détaillée-par-fournisseur) +8. [Option hybride recommandée](#8-option-hybride-recommandée) +9. [Matrice de décision](#9-matrice-de-décision) +10. [Recommandation finale](#10-recommandation-finale) + +--- + +## 1. Ce que l'app exige comme infrastructure + +Avant de comparer, voici ce que Xpeditis consomme réellement (issu de l'analyse du code) : + +| Composant | Besoin réel | Impact coût | +|---|---|---| +| **Kubernetes** | 2–15 pods NestJS + 1–8 pods Next.js | Control plane + nodes | +| **PostgreSQL 15** | 18 tables, audit_logs volumineuses, pg_trgm | Instance avec au moins 4 GB RAM en prod | +| **Redis 7** | Cache rate quotes TTL 15 min + pub/sub WebSocket | Au moins 1 GB, réplication si multi-pods | +| **Stockage objet (S3)** | PDFs booking, docs carrier (max 10 MB), exports | ~10 GB/100 users → ~1 TB/10 000 users | +| **Load Balancer** | WebSocket sticky sessions obligatoires | 1 LB avec support WS | +| **Email** | 10 types d'emails, PDFs en pièce jointe | SES ou SMTP tiers | +| **Secrets** | JWT, DB passwords, 5 API keys carriers | Secrets Manager ou équivalent | +| **Appels externes** | 5 carriers APIs à chaque rate search (circuit breaker 5s) | Trafic sortant → coût NAT/egress | +| **DNS + TLS** | Route 53 ou équivalent + cert-manager | ~$1-3/mois | + +**Spécificité critique :** Le SDK AWS S3 v3 est déjà utilisé dans le code avec support d'endpoint personnalisé → **zéro modification de code** pour utiliser n'importe quel stockage S3-compatible (Hetzner Object Storage, DO Spaces, OVH, Scaleway). + +--- + +## 2. Vue d'ensemble des fournisseurs + +| Fournisseur | Kubernetes | DB Managée | RGPD EU | Egress gratuit | Self-managed DB | Difficulté ops | +|---|---|---|---|---|---|---| +| **Hetzner** 🇩🇪 | k3s free / HKE free | ❌ (tiers: Neon, Railway) | ✅ Allemagne/Finlande | Non ($0.045/GB) | ✅ DIY | ⭐⭐⭐ Élevée | +| **OVHcloud** 🇫🇷 | MKS gratuit (CP) | ✅ Partiel (limité) | ✅ France | ✅ **Oui** | ✅ DIY sur VM | ⭐⭐ Moyenne | +| **DigitalOcean** 🇺🇸 | DOKS gratuit (CP) | ✅ PG + Redis | ❌ US (AMS dispo) | Non ($0.01/GB) | Optionnel | ⭐ Faible | +| **Scaleway** 🇫🇷 | Kapsule gratuit (CP) | ✅ PostgreSQL | ✅ Paris | ✅ Inclus | Optionnel | ⭐ Faible | +| **GCP** 🌐 | GKE Autopilot/Standard | ✅ Cloud SQL | ✅ europe-west1 | Non ($0.12/GB) | Optionnel | ⭐ Faible | +| **AWS** 🌐 | EKS ($73/mois CP) | ✅ RDS + ElastiCache | ✅ eu-west-3 Paris | Non ($0.09/GB) | Optionnel | ⭐ Faible | +| **Azure** 🌐 | AKS gratuit (CP) | ✅ Flexible Server | ✅ westeurope | Non ($0.087/GB) | Optionnel | ⭐ Faible | +| **Vultr** 🇺🇸 | VKE ($10/mois CP) | ✅ Partiel | ❌ US | Non | Optionnel | ⭐ Faible | + +--- + +## 3. Tableau comparatif — 100 utilisateurs + +**Hypothèses :** 10–20 utilisateurs simultanés, ~50 bookings/mois, 500 rate searches/jour, 5 GB stockage, 500 emails/mois + +### Configuration retenue +- 2 worker nodes (backend NestJS × 2 pods + frontend Next.js × 1 pod) +- PostgreSQL 2 vCPU / 4-8 GB +- Redis 0.5-1 GB +- Stockage objet 20 GB +- 1 Load Balancer + +| Fournisseur | Compute | DB | Redis | Stockage | LB/Réseau | **Total/mois** | +|---|---|---|---|---|---|---| +| **Hetzner** *(k3s self-managed)* | 2× CX32: **€13.60** | CX22 self-host: **€3.79** | Shared: **€0** | Object Storage: **€4.99** | LB11: **€5.39** | **🥇 ~€28 (~$30)** | +| **Hetzner** *(post 1 avril)* | 2× CX32: **€18.38** | CX22: **€5.11** | Shared: **€0** | **€4.99** | LB11: **€7.49** | **~€36 (~$39)** | +| **DigitalOcean** *(DOKS)* | 2× s-2vcpu-4gb: **$48** | PG Basic 2 GB: **$30** | Redis 1 GB: **$15** | Spaces: **$5** | LB: **$12** | **~$110** | +| **OVHcloud** *(MKS)* | 2× B2-7: **€48** | B2-7 self-host: **€24** | Shared pod: **€0** | OBJ 20 GB: **€0.22** | LB: **€15** | **~€87 (~$94)** | +| **Scaleway** *(Kapsule)* | 2× DEV1-L: **€61** | DB-DEV-S self-host: **€14** | DEV1-S: **€7** | OBJ 20 GB: **€0.20** | LB: **€9.99** | **~€92 (~$100)** | +| **GCP** *(GKE Autopilot)* | Autopilot pods: **~$60** | Cloud SQL db-f1-small: **$26** | Memorystore 1 GB: **$39** | GCS 20 GB: **$0.40** | LB: **$18** | **~$143** | +| **Vultr** *(VKE)* | 2× 2vCPU/4GB: **$40** | PG managed ~$30: **$30** | Redis: **$15** | Block: **$5** | VKE CP: **$10** | **~$100** | +| **AWS** *(EKS, eu-west-3)* | 2× t3.medium: **$69** | db.t4g.medium: **$60** | cache.t4g.micro: **$12** | S3 20 GB: **$0.46** | EKS: **$73** + ALB: **$22** + NAT: **$34** | **~$270** | +| **Azure** *(AKS)* | 2× D2s_v3: **$140** | PG B1ms flex: **$12**\* | Redis C0: **$14** | Blob 20 GB: **$0.36** | AKS: **$0** + LB: **$18** | **~$185** | +| **GCP** *(GKE Standard)* | GKE CP: **$72** + 2× e2-std-2: **$97** | Cloud SQL: **$100** | Memorystore: **$39** | GCS: **$0.40** | LB: **$18** | **~$327** | + +> \* Azure B1ms est très limité (1 vCPU), insuffisant pour une production sérieuse. Compte tenu des vraies instances : ~$165+ + +**⚠️ AWS à 100 users :** Le control plane EKS ($73) + NAT Gateway ($34) représentent ~40% de la facture — cher pour si peu d'utilisateurs. + +--- + +## 4. Tableau comparatif — 1 000 utilisateurs + +**Hypothèses :** 100–300 simultanés, ~500 bookings/mois, 5 000 searches/jour, 200 GB stockage, 15 000 emails/mois, PostgreSQL Multi-AZ ou HA + +### Configuration retenue +- 3–4 worker nodes avec HPA +- PostgreSQL 4 vCPU / 8-16 GB **avec HA/réplication** +- Redis 1-2 GB répliqué (pub/sub WebSocket multi-pods) +- Stockage objet 200 GB +- 1 Load Balancer + +| Fournisseur | Compute | DB (HA) | Redis | Stockage | LB/Réseau | **Total/mois** | +|---|---|---|---|---|---|---| +| **Hetzner** *(k3s self-managed)* | 1×CX22 CP + 3×CX42: **€60.60** | CX32 PG + 100 GB vol: **€11.20** | CX22 Redis: **€3.79** | Object Storage: **€4.99** | LB21: **€16.40** | **🥇 ~€97 (~$105)** | +| **Hetzner** *(post 1 avril)* | **€81.69** | **€15.12** | **€5.11** | **€4.99** | **€22.14** | **~€129 (~$140)** | +| **OVHcloud** *(MKS + self-hosted)* | 3× B2-15: **€138** | B2-15 PG primary + B2-7 replica: **€70** | B2-7 Redis: **€24** | OBJ 200 GB: **€2.20** | LB: **€15** | **~€249 (~$270)** | +| **DigitalOcean** *(DOKS + managed)* | 3× s-4vcpu-8gb: **$144** | PG 4 GB HA: **$120** | Redis 2 GB: **$30** | Spaces 200 GB: **$10** | LB: **$12** | **~$316** | +| **Scaleway** *(Kapsule + managed)* | 3× PLAY2-MICRO: **€118** | DB-PRO2-XXS managed: **€80** | PLAY2-NANO Redis: **€20** | OBJ 200 GB: **€2** | LB: **€9.99** | **~€230 (~$249)** | +| **Vultr** *(VKE)* | 3× 4vCPU/8GB: **$120** | PG managed ~$60: **$60** | Redis: **$30** | Block 200 GB: **$20** | VKE CP: **$10** | **~$240** | +| **GCP** *(GKE Autopilot)* | Autopilot 8 pods: **~$200** | Cloud SQL n1-std-2 HA: **$250** | Memorystore 2 GB: **$78** | GCS: **$5** | LB: **$20** | **~$553** | +| **AWS** *(EKS, eu-west-3)* | EKS CP: **$73** + 3×t3.xlarge: **$414** | db.r6g.large Multi-AZ: **$496** | cache.r6g.large: **$150** | S3 200 GB: **$4.60** | ALB: **$38** + 2×NAT: **$73** | **~$1 249** | +| **Azure** *(AKS + managed)* | AKS CP: **$72** + 3×D4s_v3: **$420** | PG D2ds_v6 HA: **$327** | Redis C2 Standard: **$109** | Blob: **$4** | LB: **$25** | **~$957** | +| **GCP** *(GKE Standard)* | GKE CP: **$72** + 3×e2-std-4: **$292** | Cloud SQL n1-std-4 HA: **$460** | Memorystore 2 GB: **$78** | GCS: **$5** | LB: **$20** + NAT: **$20** | **~$947** | + +--- + +## 5. Tableau comparatif — 10 000 utilisateurs + +**Hypothèses :** 1 000–3 000 simultanés, ~5 000 bookings/mois, 50 000 searches/jour, 1-2 TB stockage, 150 000 emails/mois + +### Configuration retenue +- 6–8 worker nodes avec autoscaling +- PostgreSQL 8 vCPU / 32 GB HA + read replica +- Redis cluster/réplication (WebSocket + cache massif) +- Stockage objet 1 TB avec lifecycle policies +- 2 Load Balancers + +| Fournisseur | Compute | DB (HA + replica) | Redis | Stockage | LB/Réseau | **Total/mois** | +|---|---|---|---|---|---|---| +| **Hetzner** *(k3s self-managed)* | 3×CX22 CP + 6×CX52: **€227** | CCX23 PG+replica CX32: **€35** + 500 GB vol: **€22** | CX42 Redis: **€16.40** | Object Storage + extra: **€15** | LB31: **€29** | **🥇 ~€344 (~$373)** | +| **Hetzner** *(post 1 avril)* | **€306** | **€47 + €29** | **€21.49** | **€15** | **€39** | **~€458 (~$496)** | +| **OVHcloud** *(MKS + self-hosted)* | 5× B2-30: **€470** | B2-30 PG + B2-15 replica: **€140** | B2-15 Redis: **€46** | OBJ 1 TB: **€11** | 2× LB: **€30** | **~€697 (~$756)** | +| **DigitalOcean** *(DOKS + managed)* | 6× g-4vcpu-16gb: **$720** | PG 8 GB HA: **$240** | Redis 4 GB HA: **$60** | Spaces + CDN: **$40** | 2× LB: **$24** | **~$1 084** | +| **Scaleway** *(Kapsule + managed)* | 5× GP1-S: **€683** | DB-PRO2-S managed (8 vCPU): **€320** | PLAY2-MICRO Redis: **€39** | OBJ 1 TB: **€10** | 2× LB: **€20** | **~€1 072 (~$1 163)** | +| **Vultr** *(VKE)* | 6× 4vCPU/16GB HP: **$528** | PG managed large ~$120: **$120** | Redis cluster: **$60** | Block: **$50** | VKE CP: **$10** | **~$768** | +| **GCP** *(GKE Autopilot)* | Autopilot 20 pods: **~$600** | Cloud SQL n1-std-8 HA: **$800** | Memorystore 5 GB: **$195** | GCS + CDN: **$30** | LB: **$50** | **~$1 675** | +| **AWS** *(EKS, eu-west-3)* | EKS CP: **$73** + 6×m6i.xlarge: **$981** | db.r6g.2xlarge Multi-AZ: **$1 518** + replica: **$700** | cache.r6g.xlarge cluster: **$1 452** | S3 + CDN: **$72** | 2×ALB: **$110** + 2×NAT: **$111** | **~$5 017** | +| **Azure** *(AKS + managed)* | AKS CP: **$72** + 6×D4s_v3: **$840** | PG D4ds_v6 HA: **$654** + replica: **$327** | Redis P1: **$394** | Blob + CDN: **$50** | 2× LB: **$40** | **~$2 377** | +| **GCP** *(GKE Standard)* | GKE CP: **$72** + 6×e2-std-4: **$583** | Cloud SQL n1-std-8 HA: **$1 600** | Memorystore 5 GB: **$195** | GCS + CDN: **$30** | LB: **$60** | **~$2 540** | + +--- + +## 6. Récapitulatif global + +### Coût mensuel all-in (production viable) + +| Fournisseur | 100 users | 1 000 users | 10 000 users | RGPD EU | Ops requis | +|---|---|---|---|---|---| +| 🥇 **Hetzner (self-managed, actuel)** | **€28** | **€97** | **€344** | ✅ 🇩🇪 | ⭐⭐⭐ Élevé | +| 🥈 **Hetzner (post 1 avril)** | **€36** | **€129** | **€458** | ✅ 🇩🇪 | ⭐⭐⭐ Élevé | +| 🥉 **OVHcloud (self-hosted DB)** | **€87** | **€249** | **€697** | ✅ 🇫🇷 | ⭐⭐ Moyen | +| 4️⃣ **Vultr VKE** | **$100** | **$240** | **$768** | ❌ US | ⭐ Faible | +| 5️⃣ **DigitalOcean DOKS** | **$110** | **$316** | **$1 084** | ❌ US\* | ⭐ Faible | +| 6️⃣ **Scaleway Kapsule** | **€92** | **€230** | **€1 072** | ✅ 🇫🇷 | ⭐ Faible | +| 7️⃣ **GCP GKE Autopilot** | **$143** | **$553** | **$1 675** | ✅ Belgium | ⭐ Faible | +| 8️⃣ **Azure AKS** | **$185** | **$957** | **$2 377** | ✅ Netherlands | ⭐ Faible | +| 9️⃣ **GCP GKE Standard** | **$327** | **$947** | **$2 540** | ✅ Belgium | ⭐ Faible | +| 🔟 **AWS EKS** | **$270** | **$1 249** | **$5 017** | ✅ 🇫🇷 Paris | ⭐ Faible | + +> \* DigitalOcean propose une région Amsterdam (AMS3) pour la conformité RGPD européenne. + +### Rapport qualité/prix — Score global + +``` +Hetzner post-1er avril ████████████████████ 1x (référence) +OVHcloud ████████████░░░░░░░░ 3x vs Hetzner +Vultr ████████████░░░░░░░░ 3x +DigitalOcean ████████░░░░░░░░░░░░ 3.5x +Scaleway ████████░░░░░░░░░░░░ 3.5x +GCP Autopilot ████░░░░░░░░░░░░░░░░ 5x +Azure ███░░░░░░░░░░░░░░░░░ 6x +GCP Standard ██░░░░░░░░░░░░░░░░░░ 7x +AWS EKS █░░░░░░░░░░░░░░░░░░░ 13x (à 10 000 users) +``` + +--- + +## 7. Analyse détaillée par fournisseur + +### 🟢 Hetzner Cloud — Le moins cher de loin + +**Avantages :** +- Prix **5 à 15× inférieurs** à AWS/GCP pour des ressources équivalentes +- Serveurs ARM64 Ampere (CAX-series) : encore moins chers et performants pour des workloads Node.js +- Object Storage S3-compatible inclus dès €4.99/mois (1 TB) — **remplace MinIO directement** +- Kubernetes gratuit (HKE control plane free ou k3s self-managed) +- Data centers en Allemagne et Finlande → RGPD natif +- Trafic entrant gratuit, sortant €0.045/GB (bien moins qu'AWS) +- [hetzner-k3s](https://github.com/vitobotta/hetzner-k3s) : cluster Kubernetes en 5 minutes avec 1 commande +- **⚠️ Hausse de prix ~35% le 1er avril 2026** — même après, Hetzner reste 4–10× moins cher qu'AWS + +**Inconvénients :** +- **Pas de base de données managée native** → il faut soit self-hoster, soit utiliser un service tiers +- Pas de managed Redis natif +- Support limité (pas de support 24/7 téléphonique enterprise) +- PostgreSQL self-hosted = vous gérez les backups, les mises à jour, la HA +- Moins de services managés (pas d'équivalent IAM, Secrets Manager, WAF natifs) + +**Options pour la base de données sur Hetzner :** + +| Solution | Prix | Trade-off | +|---|---|---| +| Self-hosted PG sur CX32 | €6.80-9.19/mois | Vous gérez tout | +| **Neon.tech** (serverless PG) | $0-19/mois (free tier généreux) | Parfait pour dev, pas pour 10 000 users | +| **Railway.app** (managed PG) | $5/mois + usage | Simple, backups auto | +| **Supabase** (managed PG) | $25/mois (Pro) | PostgreSQL + Auth + Storage | +| **Ubicloud PG sur Hetzner** | ~$10-50/mois | Managed, tourne sur Hetzner | + +**Verdict Hetzner :** Idéal si vous avez les compétences ops pour gérer PostgreSQL et Redis. Le ROI est massif. Recommandé dès la phase MVP. + +--- + +### 🟡 OVHcloud — Le meilleur compromis européen + +**Avantages :** +- **Entreprise française**, RGPD natif, données en France +- **Zéro frais d'egress** — si votre app génère beaucoup de trafic sortant (carrier APIs, PDFs), c'est une économie réelle +- MKS Managed Kubernetes control plane **gratuit** +- Pricing prévisible sans surprises sur la facture +- Object Storage S3-compatible à €0.011/GB (moins cher que AWS S3) +- **Bonne alternative à AWS pour les clients qui exigent la souveraineté française** + +**Inconvénients :** +- Interfaces et UX moins polies qu'AWS/DO +- CloudDB (DB managée) est limitée en ressources et fonctionnalités +- Support parfois lent selon les retours communauté +- Moins de services managés (Redis non managé natif) +- Pas de CDN aussi performant que CloudFront + +**Verdict OVHcloud :** Excellent choix si la souveraineté des données en France est un critère client ou réglementaire. L'absence d'egress est un vrai avantage sur les gros volumes. + +--- + +### 🟡 DigitalOcean — Le plus simple à utiliser + +**Avantages :** +- Interface la plus simple et intuitive du marché +- **Managed PostgreSQL + Redis de qualité** à prix raisonnable +- DOKS (Kubernetes) avec control plane **gratuit** +- DO Spaces : S3-compatible, €5/mois inclut 250 GB + 1 TB egress — **parfait pour Xpeditis** +- Excellente documentation, nombreux tutoriels +- Support réactif +- Load Balancer $12/mois sans surprise + +**Inconvénients :** +- Siège aux USA (mais région Amsterdam disponible pour RGPD) +- Prix compute assez élevés vs Hetzner (×3-4) +- Pas de Reserved Instances / économies long terme +- Redis pricing identique à PostgreSQL (peut sembler cher pour un cache) + +**Verdict DigitalOcean :** Le meilleur choix si vous voulez **minimiser le temps d'opération** et que vous avez un budget correct. Pricing transparent, aucune surprise sur la facture (contrairement à AWS). + +--- + +### 🟡 Scaleway — L'alternative française moderne + +**Avantages :** +- **Entreprise française** (Iliad group), data centers à Paris +- RGPD natif, conformité HDS disponible (santé) +- Kapsule Kubernetes control plane **gratuit** +- Instances ARM64 disponibles (très économiques) +- Object Storage S3-compatible avec egress inclus +- API moderne, bonne DX + +**Inconvénients :** +- **Managed databases chères** (€80/mois pour 2 vCPU / 8 GB) +- Moins mature qu'AWS/GCP pour les fonctionnalités avancées +- Catalogue de services plus limité +- Moins de régions disponibles + +**Verdict Scaleway :** Intéressant si vous restez en self-hosted pour la DB ou si le contexte légal exige France. Les managed databases sont trop chères pour un early-stage. + +--- + +### 🔴 AWS EKS — Le plus puissant, le plus cher + +**Avantages :** +- Écosystème le plus complet (IAM, KMS, Secrets Manager, WAF, CloudFront, SES...) +- SLA enterprise, support 24/7, certifications (ISO 27001, SOC2, HDS France) +- RDS Multi-AZ battle-tested, ElastiCache Redis géré en perfection +- CloudFront CDN mondial +- Région Paris (eu-west-3) pour conformité RGPD française +- Idéal si vous intégrez avec d'autres services AWS (Maersk utilise AWS, par exemple) + +**Inconvénients :** +- **EKS control plane $73/mois fixe** — cher pour 100 users +- **NAT Gateway $33/mois par AZ** + $0.045/GB traitement → coût caché significatif +- **Facturation complexe** : les surprises de facture AWS sont légendaires +- RDS Multi-AZ double le coût de la DB +- ElastiCache explose à 10 000 users ($1 452/mois pour Redis cluster) +- Pas compétitif sur le pure coût compute + +**Verdict AWS :** Justifié uniquement si vous avez des clients enterprise qui l'exigent contractuellement, ou si vous êtes déjà profondément intégré dans l'écosystème AWS. Pour un SaaS maritime en croissance, le surcoût n'est pas justifiable avant 10 000+ users. + +--- + +### 🔶 GCP — Meilleur rapport qualité/prix parmi les hyperscalers + +**Avantages :** +- **GKE Autopilot** : pas de gestion des nodes, facturation à la pod (potentiellement économique pour charges variables) +- Réseau performant, BigQuery pour analytics +- Cloud SQL plus simple à configurer qu'AWS RDS +- Sustained Use Discounts automatiques (pas besoin de Reserved Instances) + +**Inconvénients :** +- Cloud SQL coûteux en HA (doublement) +- Memorystore Redis pricing identique à ElastiCache +- Egress coûteux ($0.12/GB vs $0.09 AWS) +- Interface moins intuitive qu'AWS/Azure + +**Verdict GCP :** Alternative intéressante à AWS avec Autopilot pour les charges variables. Mais Cloud SQL HA reste cher. Pas compétitif face à DO/Hetzner. + +--- + +### 🔶 Azure — Le moins intéressant pour ce projet + +**Avantages :** +- AKS control plane gratuit +- Bien intégré si déjà client Microsoft +- PostgreSQL Flexible Server correct + +**Inconvénients :** +- Worker nodes (VM) les plus chers des hyperscalers +- Azure Cache for Redis pricing élevé (P1 = $394/mois) +- Pricing complexe et souvent plus élevé qu'AWS à périmètre équivalent +- Moins de services maritimes/logistiques spécifiques + +**Verdict Azure :** Aucun avantage notable pour Xpeditis. AWS est plus mature pour ce type de projet si vous allez sur un hyperscaler. + +--- + +## 8. Option hybride recommandée + +La meilleure stratégie coût/risque pour Xpeditis est une **approche hybride** : compute sur Hetzner (ou OVH), services critiques sur des managed services spécialisés. + +``` +┌─────────────────────────────────────────────────────┐ +│ Xpeditis — Architecture Hybride │ +│ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ Hetzner Cloud (compute + réseau) │ │ +│ │ │ │ +│ │ k3s cluster │ │ +│ │ ├── NestJS pods (2-15 replicas) │ │ +│ │ ├── Next.js pods (1-8 replicas) │ │ +│ │ └── Traefik Ingress + cert-manager │ │ +│ │ │ │ +│ │ Hetzner Object Storage │ │ +│ │ (S3-compatible, 0 changement de code) │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ │ +│ ┌───────────────────────────────────────────────┐ │ +│ │ Services managés externes (prix fixes) │ │ +│ │ │ │ +│ │ Neon.tech / Railway ──► PostgreSQL managé │ │ +│ │ Upstash Redis ──► Redis serverless │ │ +│ │ Brevo / Postmark ──► Email SMTP │ │ +│ │ Cloudflare ──► CDN + WAF (free) │ │ +│ └───────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +### Coûts de l'architecture hybride + +| Service | Fournisseur | 100 users | 1 000 users | 10 000 users | +|---|---|---|---|---| +| Compute + réseau | Hetzner (post-avril) | €36 | €129 | €458 | +| PostgreSQL managé | [Neon.tech](https://neon.tech) Pro | $19 | $69 | $700 | +| Redis managé | [Upstash](https://upstash.com) | $0 (free) | $10 | $120 | +| Email | [Brevo](https://brevo.com) (ex-Sendinblue) | $0 (free 300/j) | $9 | $49 | +| CDN + WAF | [Cloudflare](https://cloudflare.com) | $0 (free) | $0 | $0-20 | +| DNS | Cloudflare | $0 | $0 | $0 | +| **TOTAL** | | **~€65 (~$70)** | **~€230 (~$250)** | **~€1 400 (~$1 520)** | + +> **Avantage Neon.tech :** PostgreSQL serverless, scale automatique, branchement de DB pour dev/staging, backups automatiques. Compatible avec le TypeORM de Xpeditis sans changement. + +> **Avantage Upstash Redis :** Pricing pay-per-use, pas de cluster à gérer. Parfait pour le cache rate quotes et le pub/sub WebSocket à petite/moyenne échelle. + +--- + +## 9. Matrice de décision + +Choisissez votre scenario selon vos priorités : + +### Critère 1 — Budget serré (early-stage, bootstrapped) + +``` +Budget < €100/mois → Hetzner self-managed + Neon.tech + Upstash +``` + +**Recommandé :** Architecture hybride Hetzner ci-dessus. +**Risque :** Vous gérez vous-même k3s, les mises à jour de nœuds, la surveillance. +**Mitigation :** Utiliser `hetzner-k3s` (automatise 90% des ops K8s) et des managed services tiers. + +--- + +### Critère 2 — Balance coût / sérénité (série A, 1-5 devs) + +``` +Budget €200-500/mois → DigitalOcean DOKS ou OVHcloud MKS +``` + +**DigitalOcean** si vous voulez la simplicité maximale et que les données hors-EU ne sont pas bloquantes. +**OVHcloud** si vous avez des clients qui exigent la souveraineté française des données. + +--- + +### Critère 3 — Conformité RGPD maximale + souveraineté française + +``` +Données en France requises → OVHcloud (Paris) ou Scaleway (Paris) ou AWS eu-west-3 (Paris) +``` + +- **Budget raisonnable :** OVHcloud (€249/mois à 1 000 users vs $1 249 AWS) +- **Conformité enterprise :** AWS eu-west-3 (certifications HDS, ISO 27001 France) + +--- + +### Critère 4 — Croissance rapide, clients enterprise (scale-up) + +``` +10 000+ users + SLA enterprise → AWS eu-west-3 ou GCP europe-west1 +``` + +Justifié si : +- Clients exigent des certifications spécifiques (HDS pour santé, PCI-DSS) +- Contrats avec pénalités SLA +- Intégration avec d'autres services cloud (ex: Maersk/MSC utilisent AWS) +- Équipe ops dédiée pour gérer les coûts AWS + +--- + +### Critère 5 — Vous voulez tout gérer vous-même (max économies) + +``` +VPS self-managed → Hetzner (k3s + PostgreSQL + Redis sur VM dédiées) +``` + +**Stack complète self-managed sur Hetzner :** + +| Composant | Solution | Outil | +|---|---|---| +| Kubernetes | k3s via hetzner-k3s | [github.com/vitobotta/hetzner-k3s](https://github.com/vitobotta/hetzner-k3s) | +| PostgreSQL | Docker + pg_auto_failover ou Patroni | Ou simplement 1 VM dédiée + cron backup S3 | +| Redis | Docker single node | Replication manuelle si besoin | +| Stockage | Hetzner Object Storage | SDK déjà configuré dans Xpeditis | +| TLS | cert-manager + Let's Encrypt | Déjà dans l'écosystème k3s | +| Monitoring | Grafana + Prometheus | Stack kube-prometheus-stack | +| Emails | Brevo / Postmark SMTP | Changer SMTP_HOST dans .env | +| CDN | Cloudflare (gratuit) | Proxy devant Hetzner LB | + +--- + +## 10. Recommandation finale + +### Pour Xpeditis 2.0 — Recommandation par phase + +--- + +#### Phase MVP / Lancement (0 → 100 users) +**→ Hetzner Cloud + services managés tiers** + +``` +Budget cible : €65-80/mois +``` + +| Composant | Solution | Coût | +|---|---|---| +| K8s cluster | 2× CX32 + k3s | €18.38 (post-avril) | +| LB | LB11 Hetzner | €7.49 | +| PostgreSQL | Neon.tech Pro | $19 | +| Redis | Upstash free tier | $0 | +| Stockage | Hetzner Object Storage | €4.99 | +| Email | Brevo free (300/j) | €0 | +| CDN + WAF | Cloudflare free | €0 | +| **TOTAL** | | **~€55/mois** | + +**Pourquoi :** À ce stade, le coût doit être minimum. Le risque technique d'un PostgreSQL managé par Neon est faible et bien inférieur à se gérer soi-même. + +--- + +#### Phase Croissance (100 → 1 000 users) +**→ OVHcloud MKS ou DigitalOcean DOKS** + +``` +Budget cible : €200-350/mois +``` + +**Si RGPD/souveraineté française critique :** +- OVHcloud MKS (3× B2-15) + PostgreSQL self-hosted sur B2-15 + Redis sur B2-7 +- **~€249/mois** + +**Si priorité simplicité :** +- DigitalOcean DOKS (3× s-4vcpu-8gb) + Managed PG 4 GB HA + Managed Redis +- **~$316/mois** + +**Pourquoi pas Hetzner ici ?** La gestion de PostgreSQL HA (Patroni + etcd) + Redis Sentinel devient complexe quand le trafic augmente et que vous n'avez pas d'équipe ops dédiée. + +--- + +#### Phase Scale (1 000 → 10 000 users) +**→ OVHcloud ou DigitalOcean (selon RGPD)** + +``` +Budget cible : €700-1 100/mois +``` + +| Fournisseur | Coût 10 000 users | Avantage | +|---|---|---| +| OVHcloud | ~€697 | RGPD France, 0 egress | +| DigitalOcean | ~$1 084 | Simplicité, managed DB | +| **AWS EKS** | **~$5 017** | SLA enterprise, certifications | + +**→ AWS EKS uniquement si** vous avez des contrats enterprise qui l'exigent, car le surcoût (×5 à ×7 vs OVH/DO) doit être justifié par le CA client. + +--- + +### Résumé en une phrase par option + +| Option | Pour qui | +|---|---| +| **Hetzner self-managed** | Vous avez les skills ops, le budget est critique, phase MVP | +| **OVHcloud MKS** | Clients français exigeants sur RGPD, budget moyen, bonne expertise Linux | +| **DigitalOcean DOKS** | Vous voulez vous concentrer sur le code, pas l'infra, budget correct | +| **Scaleway Kapsule** | Entreprise française, conformité HDS possible, managed DB acceptable | +| **AWS EKS eu-west-3** | Clients enterprise, SLA contractuels, équipe et budget dédiés | +| **Architecture hybride** | Le meilleur ROI à toutes les étapes — **recommandé par défaut** | + +--- + +### La recommandation optimale — Architecture hybride progressive + +``` +Aujourd'hui (MVP) Dans 6 mois (1 000 users) Dans 18 mois (10 000 users) +───────────────── ────────────────────────── ─────────────────────────── +Hetzner k3s Migrer vers OVHcloud MKS Rester OVH ou migrer AWS ++ Neon.tech PG + PostgreSQL self-hosted HA si clients enterprise ++ Upstash Redis + Redis Sentinel AWS eu-west-3 justifié ++ Hetzner Object Storage + Hetzner Object Storage au-delà de €500K ARR +≈ €65/mois ≈ €249/mois ≈ €697-5017/mois +``` + +> **Note sur les migrations :** Passer de Hetzner à OVHcloud ou DigitalOcean est simple (K8s manifests identiques, changement de variables d'environnement). Passer vers AWS requiert une refactorisation partielle (EKS ingress, IAM, RDS connection strings). Planifiez cette migration si vous atteignez des clients enterprise, pas avant. + +--- + +*Sources : hetzner.com/cloud, digitalocean.com/pricing, ovhcloud.com/fr/public-cloud/prices, scaleway.com/fr/tarifs, cloud.google.com, azure.microsoft.com, aws.amazon.com — Mars 2026* +*Tous les prix sont indicatifs on-demand. Les prix Hetzner pré-1er avril 2026 sont encore actifs au moment de la rédaction.* diff --git a/docs/deployment/hetzner/01-architecture.md b/docs/deployment/hetzner/01-architecture.md new file mode 100644 index 0000000..2aff52b --- /dev/null +++ b/docs/deployment/hetzner/01-architecture.md @@ -0,0 +1,286 @@ +# 01 — Architecture de production sur Hetzner + +--- + +## Vue d'ensemble + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ INTERNET │ +└───────────────────────────────┬─────────────────────────────────────────┘ + │ + ┌───────────▼───────────┐ + │ Cloudflare │ + │ WAF + CDN + DNS │ + │ TLS termination │ + └───────────┬───────────┘ + │ HTTPS (443) + ┌───────────▼───────────┐ + │ Hetzner Load │ + │ Balancer (LB11) │ + │ €7.49/mois │ + └─────┬─────────┬───────┘ + │ │ + ┌──────────────▼──┐ ┌───▼──────────────┐ + │ Worker Node 1 │ │ Worker Node 2 │ + │ CX42 (8c/16G) │ │ CX42 (8c/16G) │ + │ €21.49/mois │ │ €21.49/mois │ + │ │ │ │ + │ ┌─────────────┐ │ │ ┌─────────────┐ │ + │ │ NestJS Pod │ │ │ │ NestJS Pod │ │ + │ │ (backend) │ │ │ │ (backend) │ │ + │ └─────────────┘ │ │ └─────────────┘ │ + │ ┌─────────────┐ │ │ ┌─────────────┐ │ + │ │ Next.js Pod │ │ │ │ Next.js Pod │ │ + │ │ (frontend) │ │ │ │ (frontend) │ │ + │ └─────────────┘ │ │ └─────────────┘ │ + └────────┬────────┘ └────────┬─────────┘ + │ Réseau privé Hetzner (10.0.0.0/16) + ┌────────▼────────────────────▼─────────┐ + │ Control Plane Node │ + │ CX22 (2c/4G) €5.11/mois │ + │ k3s server (etcd) │ + └────────────────────────────────────────┘ + │ + ┌───────────────────────┼───────────────────────┐ + │ │ │ +┌───────▼───────┐ ┌───────────▼──────────┐ ┌───────▼───────┐ +│ PostgreSQL │ │ Redis │ │ Hetzner │ +│ Neon.tech │ │ Upstash (serverless) │ │ Object │ +│ ou self-host │ │ ou self-hosted │ │ Storage │ +│ $19/mois │ │ $0-10/mois │ │ S3-compat. │ +│ │ │ │ │ €4.99/mois │ +└───────────────┘ └──────────────────────┘ └───────────────┘ +``` + +--- + +## Composants et rôles + +### Couche réseau + +| Composant | Rôle | Port | +|---|---|---| +| **Cloudflare** | DNS, WAF, CDN, protection DDoS, cache assets | 443 (HTTPS) | +| **Hetzner Load Balancer** | Distribution trafic entre workers, sticky sessions WebSocket | 80, 443 | +| **Réseau privé Hetzner** | Communication inter-nœuds (10.0.0.0/16), base de données | Interne | + +### Couche Kubernetes (k3s) + +| Composant | Rôle | Ressource | +|---|---|---| +| **Control Plane (CX22)** | etcd, kube-apiserver, scheduler, controller-manager | 2 vCPU / 4 GB | +| **Worker Nodes (CX42)** | Exécution des pods NestJS + Next.js | 8 vCPU / 16 GB chacun | +| **Traefik Ingress** | Routage HTTP/HTTPS, sticky sessions Socket.IO | Built-in k3s | +| **cert-manager** | TLS automatique via Let's Encrypt | In-cluster | +| **Hetzner Cloud Controller** | Provisionne LB + volumes depuis Kubernetes | In-cluster | +| **Hetzner CSI Driver** | PersistentVolumes sur Hetzner Volumes | In-cluster | + +### Couche application + +| Pod | Image | Replicas | Ports | +|---|---|---|---| +| **xpeditis-backend** | `ghcr.io//xpeditis-backend:latest` | 2–15 | 4000 | +| **xpeditis-frontend** | `ghcr.io//xpeditis-frontend:latest` | 1–8 | 3000 | + +### Couche données + +| Service | Option MVP | Option Production | Protocole | +|---|---|---|---| +| **PostgreSQL 15** | Neon.tech Pro ($19/mois) | Self-hosted sur CX32 | 5432 | +| **Redis 7** | Upstash free ($0-10/mois) | Self-hosted StatefulSet | 6379 | +| **Stockage fichiers** | Hetzner Object Storage (€4.99/mois) | Idem (scale automatique) | HTTPS/S3 API | + +--- + +## Flux réseau détaillé + +### Requête API standard (rate search) + +``` +Client Browser + │ HTTPS + ▼ +Cloudflare (cache miss → forward) + │ HTTPS, header CF-Connecting-IP + ▼ +Hetzner Load Balancer :443 + │ HTTP (TLS terminé par Cloudflare ou cert-manager) + ▼ +Traefik Ingress (api.xpeditis.com) + │ HTTP :80 interne + ▼ +NestJS Pod (port 4000) + ├── Redis (cache rate:FSN:HAM:20ft) → HIT → retour direct + └── MISS → 5× appels APIs carriers (Maersk/MSC/etc.) + └── Réponse → Store Redis TTL 15min + └── Réponse client +``` + +### Connexion WebSocket (notifications temps réel) + +``` +Client Browser + │ wss:// upgrade + ▼ +Cloudflare (WebSocket proxy activé) + │ + ▼ +Hetzner LB (sticky session cookie activé) + │ Même backend pod à chaque reconnexion + ▼ +Traefik (annotation sticky cookie) + │ + ▼ +NestJS Pod /notifications namespace (Socket.IO) + ├── Auth: JWT validation on connect + ├── Join room: user:{userId} + └── Redis pub/sub → broadcast cross-pods +``` + +### Upload de document (carrier portal) + +``` +Carrier Browser + │ + ▼ +NestJS POST /api/v1/csv-bookings/{id}/documents + │ Validation: type (PDF/XLS/IMG), taille max 10 MB + ▼ +S3StorageAdapter.upload() + │ AWS SDK v3, forcePathStyle: true + ▼ +Hetzner Object Storage + │ Endpoint: https://fsn1.your-objectstorage.com + └── Stocké: xpeditis-docs/{orgId}/{bookingId}/{filename} +``` + +--- + +## Ports et protocoles + +### Ports externes (ouverts sur Hetzner Firewall) + +| Port | Protocole | Source | Destination | Usage | +|---|---|---|---|---| +| 22 | TCP | Votre IP uniquement | Tous nœuds | SSH administration | +| 80 | TCP | 0.0.0.0/0 | LB | Redirection HTTP → HTTPS | +| 443 | TCP | 0.0.0.0/0 | LB | HTTPS + WebSocket | +| 6443 | TCP | Votre IP + workers | Control plane | Kubernetes API | + +### Ports internes (réseau privé 10.0.0.0/16 uniquement) + +| Port | Protocole | Usage | +|---|---|---| +| 5432 | TCP | PostgreSQL (si self-hosted) | +| 6379 | TCP | Redis (si self-hosted) | +| 4000 | TCP | NestJS API (pod → pod) | +| 3000 | TCP | Next.js (pod → pod) | +| 10250 | TCP | kubelet API | +| 2379-2380 | TCP | etcd (control plane) | + +--- + +## Namespaces Kubernetes + +``` +cluster +├── xpeditis-prod # Application principale +│ ├── Deployments: backend, frontend +│ ├── Services: backend-svc, frontend-svc +│ ├── ConfigMaps: backend-config, frontend-config +│ ├── Secrets: backend-secrets, frontend-secrets +│ ├── HPA: backend-hpa, frontend-hpa +│ └── Ingress: xpeditis-ingress +│ +├── cert-manager # Gestion certificats TLS +│ └── ClusterIssuer: letsencrypt-prod, letsencrypt-staging +│ +├── monitoring # Observabilité +│ ├── Prometheus +│ ├── Grafana +│ └── Loki +│ +└── kube-system # Système k3s + ├── Traefik (Ingress Controller) + ├── Hetzner Cloud Controller Manager + └── Hetzner CSI Driver +``` + +--- + +## Pourquoi k3s plutôt que k8s complet + +| Critère | k3s (choisi) | k8s complet | +|---|---|---| +| **RAM control plane** | 512 MB | 2-4 GB | +| **CPU control plane** | 1 vCPU | 2-4 vCPU → serveur plus cher | +| **Temps install** | 5 min (hetzner-k3s) | 30-60 min | +| **Maintenance** | System Upgrade Controller inclus | Manuelle | +| **Compatibilité** | 100% compatible kubectl/helm | — | +| **Traefik** | Inclus par défaut | Installation séparée | +| **Coût** | CX22 (€5.11/mois) comme control plane | Minimum CX42 (€21.49) | +| **Production** | Oui (utilisé par des milliers de startups) | Oui | + +--- + +## Stratégie de scaling + +### Horizontal Pod Autoscaler (HPA) + +``` +Métriques surveillées : +- CPU > 70% → Scale up +- CPU < 30% (5 min) → Scale down +- Mémoire > 80% → Scale up (custom metric) + +Backend : min 2 → max 15 pods +Frontend : min 1 → max 8 pods +``` + +### Cluster Autoscaler + +``` +Worker nodes : min 2 → max 8 +Déclenché par : pods en état "Pending" (pas assez de ressources) +Délai scale-down : 10 min d'utilisation < 50% +``` + +--- + +## Décisions d'architecture + +### Pourquoi Hetzner Object Storage plutôt que MinIO self-hosted + +Le code utilise déjà `AWS SDK v3` avec `forcePathStyle: true` et un endpoint configurable. Hetzner Object Storage est 100% compatible S3 → **zéro modification de code**, juste les variables d'environnement : + +```bash +# Avant (MinIO local) +AWS_S3_ENDPOINT=http://localhost:9000 + +# Après (Hetzner Object Storage) +AWS_S3_ENDPOINT=https://fsn1.your-objectstorage.com +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION=eu-central-1 +AWS_S3_BUCKET=xpeditis-prod +``` + +### Pourquoi Neon.tech pour PostgreSQL (MVP) + +- PostgreSQL 15 managé, compatible TypeORM +- Extensions `uuid-ossp` et `pg_trgm` disponibles (requis par Xpeditis) +- Backups automatiques inclus +- Connection pooling built-in (via PgBouncer) +- Pas de gestion de HA à faire manuellement +- Free tier pour le dev, $19/mois pour la prod +- Migration vers self-hosted possible à tout moment + +### Pourquoi Cloudflare devant Hetzner LB + +- CDN mondial (cache des assets Next.js) +- Protection DDoS free +- WAF avec règles OWASP +- DNS avec failover automatique +- Certificats TLS optionnels (on peut laisser cert-manager gérer le TLS) +- Cache des PDFs générés → économise les appels S3 diff --git a/docs/deployment/hetzner/02-prerequisites.md b/docs/deployment/hetzner/02-prerequisites.md new file mode 100644 index 0000000..bc2fe7d --- /dev/null +++ b/docs/deployment/hetzner/02-prerequisites.md @@ -0,0 +1,233 @@ +# 02 — Prérequis + +Tout ce dont vous avez besoin avant de commencer le déploiement. + +--- + +## Comptes à créer + +### Obligatoires + +| Service | URL | Usage | Coût | +|---|---|---|---| +| **Hetzner Cloud** | https://console.hetzner.cloud | Serveurs, LB, Object Storage | Pay-as-you-go | +| **GitHub** | https://github.com | Code + GitHub Actions + GHCR (images Docker) | Gratuit | +| **Cloudflare** | https://cloudflare.com | DNS + WAF + CDN | Gratuit (plan Free) | +| **Neon.tech** | https://neon.tech | PostgreSQL managé | Free → $19/mois Pro | + +### Recommandés (peuvent être substitués) + +| Service | URL | Usage | Coût | +|---|---|---|---| +| **Upstash** | https://upstash.com | Redis serverless | Free → $10/mois | +| **Brevo** | https://brevo.com | Email SMTP (remplace SendGrid) | Gratuit jusqu'à 300/j | +| **Sentry** | https://sentry.io | Error tracking | Gratuit (5K events/mois) | + +--- + +## Outils locaux à installer + +### Outils essentiels + +```bash +# macOS (avec Homebrew) +brew install kubectl helm hcloud vitobotta/tap/hetzner-k3s + +# Vérification versions minimales requises +kubectl version --client # >= 1.28 +helm version # >= 3.12 +hcloud version # >= 1.40 +hetzner-k3s version # >= 3.0 +``` + +```bash +# Ubuntu/Debian +# kubectl +curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" +chmod +x kubectl && sudo mv kubectl /usr/local/bin/ + +# helm +curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + +# hcloud CLI +curl -Lo hcloud.tar.gz https://github.com/hetznercloud/cli/releases/latest/download/hcloud-linux-amd64.tar.gz +tar -xzf hcloud.tar.gz && sudo mv hcloud /usr/local/bin/ + +# hetzner-k3s +curl -Lo hetzner-k3s https://github.com/vitobotta/hetzner-k3s/releases/latest/download/hetzner-k3s-linux-amd64 +chmod +x hetzner-k3s && sudo mv hetzner-k3s /usr/local/bin/ +``` + +### Outils optionnels mais recommandés + +```bash +# kubectx + kubens — changer de contexte/namespace facilement +brew install kubectx + +# k9s — interface terminal pour Kubernetes (très utile) +brew install k9s + +# stern — logs multi-pods en temps réel +brew install stern + +# AWS CLI v2 — pour interagir avec Hetzner Object Storage +brew install awscli + +# Docker — pour build et test des images en local +brew install --cask docker +``` + +--- + +## Clés SSH + +Générez une paire de clés SSH dédiée pour Hetzner (ne réutilisez pas votre clé perso) : + +```bash +# Générer une clé ED25519 (plus sécurisée et performante que RSA) +ssh-keygen -t ed25519 -C "xpeditis-hetzner-deploy" -f ~/.ssh/xpeditis_hetzner + +# Résultat : +# ~/.ssh/xpeditis_hetzner (clé privée — ne JAMAIS partager) +# ~/.ssh/xpeditis_hetzner.pub (clé publique — à ajouter sur Hetzner) + +# Vérification +cat ~/.ssh/xpeditis_hetzner.pub +# ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... xpeditis-hetzner-deploy +``` + +Ajoutez la clé publique dans `~/.ssh/config` pour faciliter la connexion : + +```bash +cat >> ~/.ssh/config << 'EOF' +Host hetzner-xpeditis-* + IdentityFile ~/.ssh/xpeditis_hetzner + User root + StrictHostKeyChecking no +EOF +``` + +--- + +## Nom de domaine + +Vous avez besoin d'un domaine avec accès à la gestion DNS. Ce guide suppose : + +| Domaine | Usage | +|---|---| +| `xpeditis.com` (ou votre domaine) | Site principal | +| `api.xpeditis.com` | API NestJS backend | +| `app.xpeditis.com` | Frontend Next.js | +| `monitoring.xpeditis.com` | Grafana (optionnel, accès restreint) | + +**Si vous n'avez pas encore de domaine :** Namecheap (~$10/an) ou OVHcloud (~€7/an). Une fois acheté, déléguez les DNS à Cloudflare (gratuit, meilleur outil DNS). + +### Déléguer le DNS à Cloudflare + +1. Créez un compte sur https://cloudflare.com +2. "Add a Site" → entrez votre domaine +3. Cloudflare scanne vos DNS existants +4. Copiez les 2 nameservers Cloudflare (ex: `carl.ns.cloudflare.com`) +5. Chez votre registrar, remplacez les nameservers par ceux de Cloudflare +6. Attendez 5-30 min pour la propagation + +--- + +## Variables d'environnement de travail + +Créez un fichier `.env.deploy` (ne pas committer) pour centraliser vos variables de déploiement : + +```bash +# Fichier : ~/.xpeditis-deploy.env +# Source ce fichier avant de travailler : source ~/.xpeditis-deploy.env + +# Hetzner +export HCLOUD_TOKEN="" +export HETZNER_SSH_KEY_PATH="$HOME/.ssh/xpeditis_hetzner" + +# Kubernetes +export KUBECONFIG="$HOME/.kube/kubeconfig-xpeditis-prod" + +# Domaine +export DOMAIN="xpeditis.com" +export API_DOMAIN="api.xpeditis.com" +export APP_DOMAIN="app.xpeditis.com" + +# Registry Docker (GitHub Container Registry) +export GHCR_REGISTRY="ghcr.io" +export GHCR_ORG="" +export BACKEND_IMAGE="$GHCR_REGISTRY/$GHCR_ORG/xpeditis-backend" +export FRONTEND_IMAGE="$GHCR_REGISTRY/$GHCR_ORG/xpeditis-frontend" + +# PostgreSQL (Neon.tech) +export DATABASE_URL="postgresql://user:pass@host/dbname?sslmode=require" + +# Redis (Upstash) +export REDIS_HOST="your-redis.upstash.io" +export REDIS_PORT="6379" +export REDIS_PASSWORD="" + +# Hetzner Object Storage +export HETZNER_S3_BUCKET="xpeditis-prod" +export HETZNER_S3_ENDPOINT="https://fsn1.your-objectstorage.com" +export HETZNER_S3_ACCESS_KEY="" +export HETZNER_S3_SECRET_KEY="" +``` + +--- + +## Checklist avant de démarrer + +``` +□ Compte Hetzner Cloud créé et vérifié (CB enregistrée) +□ Compte GitHub avec repo xpeditis2.0 (ou fork) +□ Compte Cloudflare avec domaine délégué +□ Compte Neon.tech créé +□ Compte Upstash créé (optionnel) +□ kubectl installé (>= 1.28) +□ helm installé (>= 3.12) +□ hcloud CLI installé et configuré +□ hetzner-k3s installé +□ Paire de clés SSH ED25519 générée pour Hetzner +□ Docker installé (pour build local) +□ Domaine + sous-domaines api. et app. planifiés +``` + +--- + +## Configuration hcloud CLI + +```bash +# Configurer le token Hetzner +hcloud context create xpeditis-prod +# Entrez votre token quand demandé + +# Vérifier la configuration +hcloud context list +hcloud server list # Doit retourner une liste vide (ou vos serveurs) + +# Lister les types de serveurs disponibles +hcloud server-type list + +# Lister les images disponibles +hcloud image list --type system | grep ubuntu +``` + +--- + +## Vérification finale + +```bash +# Tous ces checks doivent passer avant de continuer +echo "=== Vérification des outils ===" +kubectl version --client --short 2>/dev/null && echo "✅ kubectl OK" || echo "❌ kubectl manquant" +helm version --short 2>/dev/null && echo "✅ helm OK" || echo "❌ helm manquant" +hcloud version 2>/dev/null && echo "✅ hcloud OK" || echo "❌ hcloud manquant" +hetzner-k3s version 2>/dev/null && echo "✅ hetzner-k3s OK" || echo "❌ hetzner-k3s manquant" +docker --version 2>/dev/null && echo "✅ docker OK" || echo "❌ docker manquant" +ssh-keygen -l -f ~/.ssh/xpeditis_hetzner.pub 2>/dev/null && echo "✅ SSH key OK" || echo "❌ Clé SSH manquante" + +echo "" +echo "=== Vérification des tokens ===" +hcloud context list 2>/dev/null | grep xpeditis && echo "✅ hcloud context OK" || echo "❌ hcloud context non configuré" +``` diff --git a/docs/deployment/hetzner/03-hetzner-setup.md b/docs/deployment/hetzner/03-hetzner-setup.md new file mode 100644 index 0000000..08145ec --- /dev/null +++ b/docs/deployment/hetzner/03-hetzner-setup.md @@ -0,0 +1,290 @@ +# 03 — Setup Hetzner Cloud + +--- + +## Création du compte et du projet + +### 1. Créer le compte Hetzner + +1. Rendez-vous sur https://console.hetzner.cloud +2. Créez votre compte (email + CB requis) +3. Activez la vérification 2FA (obligatoire en production) +4. Créez un **nouveau projet** : `xpeditis-prod` + +### 2. Générer le token API + +1. Dans le projet `xpeditis-prod` → **Security** → **API Tokens** +2. **Generate API Token** + - Name: `hetzner-k3s-deploy` + - Permissions: **Read & Write** +3. Copiez le token immédiatement (affiché une seule fois) + +```bash +# Configurez hcloud avec ce token +hcloud context create xpeditis-prod +# → Entrez votre token + +# Vérification +hcloud server list +# Output: ID NAME STATUS IPV4 IPV6 DATACENTER +# (liste vide si première fois) +``` + +### 3. Ajouter la clé SSH + +```bash +# Via CLI hcloud +hcloud ssh-key create \ + --name xpeditis-deploy \ + --public-key-from-file ~/.ssh/xpeditis_hetzner.pub + +# Vérification +hcloud ssh-key list +# ID NAME FINGERPRINT +# 1234567 xpeditis-deploy xx:xx:xx:... +``` + +--- + +## Réseau privé (obligatoire pour la sécurité inter-nœuds) + +Le réseau privé permet aux nœuds de communiquer entre eux sans passer par internet. + +```bash +# Créer le réseau privé +hcloud network create \ + --name xpeditis-network \ + --ip-range 10.0.0.0/16 + +# Récupérer l'ID du réseau (nécessaire pour la config k3s) +hcloud network list +# ID NAME IP RANGE SERVERS +# 12345 xpeditis-network 10.0.0.0/16 0 servers + +export HETZNER_NETWORK_ID=12345 # Remplacer par votre ID + +# Créer un sous-réseau pour les nœuds du cluster +hcloud network add-subnet xpeditis-network \ + --type cloud \ + --network-zone eu-central \ + --ip-range 10.0.1.0/24 +``` + +--- + +## Firewall (règles de sécurité) + +Créez un firewall strict. Les workers ne doivent être accessibles que via le Load Balancer et depuis votre IP pour SSH. + +```bash +# Créer le firewall +hcloud firewall create --name xpeditis-firewall + +# Règle 1 : SSH depuis votre IP uniquement +hcloud firewall add-rule xpeditis-firewall \ + --direction in \ + --protocol tcp \ + --port 22 \ + --source-ips "$(curl -s https://api.ipify.org)/32" \ + --description "SSH depuis mon IP" + +# Règle 2 : HTTP/HTTPS depuis partout (via LB → workers) +hcloud firewall add-rule xpeditis-firewall \ + --direction in \ + --protocol tcp \ + --port 80 \ + --source-ips 0.0.0.0/0 \ + --description "HTTP public" + +hcloud firewall add-rule xpeditis-firewall \ + --direction in \ + --protocol tcp \ + --port 443 \ + --source-ips 0.0.0.0/0 \ + --description "HTTPS public" + +# Règle 3 : Kubernetes API (votre IP + réseau privé Hetzner) +hcloud firewall add-rule xpeditis-firewall \ + --direction in \ + --protocol tcp \ + --port 6443 \ + --source-ips "$(curl -s https://api.ipify.org)/32" \ + --description "kube-apiserver depuis mon IP" + +hcloud firewall add-rule xpeditis-firewall \ + --direction in \ + --protocol tcp \ + --port 6443 \ + --source-ips 10.0.0.0/16 \ + --description "kube-apiserver depuis réseau privé" + +# Règle 4 : Communication inter-nœuds (réseau privé uniquement) +hcloud firewall add-rule xpeditis-firewall \ + --direction in \ + --protocol tcp \ + --port 1-65535 \ + --source-ips 10.0.0.0/16 \ + --description "Trafic interne cluster" + +hcloud firewall add-rule xpeditis-firewall \ + --direction in \ + --protocol udp \ + --port 1-65535 \ + --source-ips 10.0.0.0/16 \ + --description "Trafic UDP interne cluster" + +# Règle 5 : ICMP (ping) pour monitoring +hcloud firewall add-rule xpeditis-firewall \ + --direction in \ + --protocol icmp \ + --source-ips 0.0.0.0/0 \ + --description "ICMP ping" + +# Vérification +hcloud firewall describe xpeditis-firewall +``` + +--- + +## Object Storage — Setup du bucket S3 + +### Créer le bucket + +1. Dans la console Hetzner → votre projet → **Object Storage** +2. Cliquez **Create Bucket** + - Location: **Falkenstein (fsn1)** (même région que vos serveurs) + - Bucket name: `xpeditis-prod` + - Visibility: **Private** (obligatoire) +3. Cliquez **Create** + +### Créer les credentials S3 + +1. Dans Object Storage → **Access Keys** +2. **Generate Access Key** + - Name: `xpeditis-backend` +3. Notez bien les deux valeurs (affichées une seule fois) : + - **Access Key** (commence par `htz...`) + - **Secret Key** (longue chaîne) + +### Vérifier avec AWS CLI + +```bash +# Configurer AWS CLI pour Hetzner Object Storage +aws configure --profile hetzner +# AWS Access Key ID: +# AWS Secret Access Key: +# Default region name: eu-central-1 +# Default output format: json + +# Tester la connexion +aws s3 ls --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com + +# Créer un dossier de test +aws s3 cp /dev/null s3://xpeditis-prod/test/.gitkeep \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com + +# Vérifier +aws s3 ls s3://xpeditis-prod/ \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com +``` + +### Structure du bucket recommandée + +``` +xpeditis-prod/ +├── documents/ # Documents carrier (PDF, XLS, images) +│ └── {orgId}/ +│ └── {bookingId}/ +│ └── {filename} +├── pdfs/ # PDFs de confirmation booking +│ └── {year}/ +│ └── {month}/ +│ └── {bookingNumber}.pdf +├── exports/ # Exports CSV/Excel des bookings +│ └── {orgId}/ +│ └── {timestamp}-bookings.xlsx +├── logos/ # Logos des organisations +│ └── {orgId}/ +│ └── logo.{ext} +└── backups/ # Backups PostgreSQL (voir doc 13) + └── {date}/ + └── xpeditis-{timestamp}.sql.gz +``` + +--- + +## Volumes Hetzner (si PostgreSQL self-hosted) + +Si vous choisissez d'héberger PostgreSQL sur Hetzner (voir doc 07), créez un volume dédié : + +```bash +# Créer un volume de 50 GB pour PostgreSQL +hcloud volume create \ + --name xpeditis-postgres-data \ + --size 50 \ + --location fsn1 \ + --format ext4 + +# L'ID sera utilisé dans la config k3s pour le PersistentVolume +hcloud volume list +# ID NAME SIZE SERVER LOCATION +# 67890 xpeditis-postgres-data 50 GB - fsn1 +``` + +--- + +## Placement Groups (haute disponibilité) + +Les placement groups garantissent que vos workers sont sur des hôtes physiques différents : + +```bash +# Créer un placement group "spread" (workers sur différents hôtes physiques) +hcloud placement-group create \ + --name xpeditis-workers \ + --type spread + +# Noter l'ID pour la config hetzner-k3s +hcloud placement-group list +# ID NAME TYPE SERVERS +# 111 xpeditis-workers spread 0 +``` + +--- + +## Récapitulatif des IDs à noter + +Après cette étape, vous devez avoir : + +```bash +# À sauvegarder dans ~/.xpeditis-deploy.env +export HCLOUD_TOKEN="" +export HCLOUD_NETWORK_ID="12345" # ID du réseau privé +export HCLOUD_SSH_KEY_NAME="xpeditis-deploy" +export HCLOUD_FIREWALL_NAME="xpeditis-firewall" +export HCLOUD_PLACEMENT_GROUP_NAME="xpeditis-workers" + +# Object Storage +export HETZNER_S3_ENDPOINT="https://fsn1.your-objectstorage.com" +export HETZNER_S3_BUCKET="xpeditis-prod" +export HETZNER_S3_ACCESS_KEY="" +export HETZNER_S3_SECRET_KEY="" +``` + +--- + +## Vérification globale + +```bash +# Tout doit être en place avant de continuer vers le doc 04/05 +echo "=== Network ===" && hcloud network list +echo "=== SSH Keys ===" && hcloud ssh-key list +echo "=== Firewalls ===" && hcloud firewall list +echo "=== Volumes ===" && hcloud volume list +echo "=== Placement Groups ===" && hcloud placement-group list +echo "=== Object Storage ===" && aws s3 ls s3://xpeditis-prod/ \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com 2>/dev/null && echo "✅ Bucket accessible" || echo "❌ Bucket inaccessible" +``` diff --git a/docs/deployment/hetzner/04-server-selection.md b/docs/deployment/hetzner/04-server-selection.md new file mode 100644 index 0000000..da3ac0a --- /dev/null +++ b/docs/deployment/hetzner/04-server-selection.md @@ -0,0 +1,183 @@ +# 04 — Choix des serveurs Hetzner + +--- + +## Types de serveurs Hetzner (post 1er avril 2026) + +### Série CX — Intel/AMD partagé (usage général) + +| Type | vCPU | RAM | SSD | Bande passante | Prix/mois | +|---|---|---|---|---|---| +| CX22 | 2 | 4 GB | 40 GB | 20 TB | **€5.11** | +| CX32 | 4 | 8 GB | 80 GB | 20 TB | **€9.19** | +| CX42 | 8 | 16 GB | 160 GB | 20 TB | **€21.49** | +| CX52 | 16 | 32 GB | 320 GB | 20 TB | **€43.49** | + +### Série CAX — ARM64 Ampere (meilleur rapport prix/perfs) + +| Type | vCPU | RAM | SSD | Prix/mois | +|---|---|---|---|---| +| CAX11 | 2 | 4 GB | 40 GB | **€3.79** | +| CAX21 | 4 | 8 GB | 80 GB | **€6.49** | +| CAX31 | 8 | 16 GB | 80 GB | **€12.49** | +| CAX41 | 16 | 32 GB | 160 GB | **€24.49** | + +> **Note ARM64 :** NestJS (Node.js) et Next.js fonctionnent parfaitement sur ARM64. Les images Docker `node:20-alpine` sont multi-arch. Les carrier APIs (Maersk, MSC...) appellent des APIs externes → architecture du serveur sans impact. **Les CAX sont 35-40% moins chères que les CX pour des perfs équivalentes.** + +### Série CCX — vCPU dédiés (pour la base de données) + +| Type | vCPU dédiés | RAM | SSD NVMe | Prix/mois | +|---|---|---|---|---| +| CCX13 | 2 | 8 GB | 80 GB | **€12.49** | +| CCX23 | 4 | 16 GB | 160 GB | **€23.99** | +| CCX33 | 8 | 32 GB | 240 GB | **€51.99** | +| CCX43 | 16 | 64 GB | 360 GB | **€103.99** | + +> Les CCX sont recommandés pour PostgreSQL self-hosted car les vCPUs dédiés évitent la contention avec d'autres clients. I/O NVMe plus rapide. + +--- + +## Recommandations par palier + +### Palier MVP — 100 utilisateurs + +**Objectif :** Démarrer avec un coût minimal, tout peut tenir sur peu de nœuds. + +``` +Control Plane : 1× CX22 (2 vCPU, 4 GB) — €5.11/mois +Workers : 2× CX32 (4 vCPU, 8 GB) — €9.19/mois × 2 = €18.38/mois +Load Balancer : LB11 — €7.49/mois +────────────────────────────────────────────────────── +Sous-total cluster : €30.98/mois ++ Neon.tech Pro : $19/mois ++ Upstash Redis : $0 (free tier) ++ Object Storage : €4.99/mois (inclut 1 TB) +────────────────────────────────────────────────────── +TOTAL : ~€55/mois +``` + +**Pods qui tiennent sur cette config :** +- 2× NestJS backend (500m CPU / 512Mi RAM chacun) +- 1× Next.js frontend (250m CPU / 256Mi RAM) +- Traefik ingress (built-in) +- cert-manager + +**Alternative ARM64 encore moins chère :** +``` +Control Plane : 1× CAX11 (2 vCPU, 4 GB) — €3.79/mois +Workers : 2× CAX21 (4 vCPU, 8 GB) — €6.49/mois × 2 = €12.98/mois +────────────────────────────────────────────────────── +Sous-total (ARM64) : €16.77/mois (vs €23.49 en CX) +``` + +> ⚠️ **Si vous utilisez CAX (ARM64)**, vérifiez que vos Dockerfiles sont buildés en `linux/arm64` ou utilisez des images `linux/amd64` avec émulation QEMU. Le plus simple : build multi-arch avec `docker buildx` (voir doc 11). + +--- + +### Palier Croissance — 1 000 utilisateurs + +**Objectif :** Performance correcte, HA partielle, autoscaling activé. + +``` +Control Plane : 1× CX22 (2 vCPU, 4 GB) — €5.11/mois +Workers : 3× CX42 (8 vCPU, 16 GB) — €21.49/mois × 3 = €64.47/mois + (autoscaling jusqu'à 6 nodes) +Load Balancer : LB21 (75 targets, 2 TB) — €22.14/mois +PostgreSQL : CCX13 dédié (4 vCPU dédiés) — €23.99/mois ← self-hosted + + 100 GB Volume — €5.70/mois +Redis : CX22 dédié — €5.11/mois +Object Storage: €4.99/mois +────────────────────────────────────────────────────── +TOTAL : ~€131/mois +``` + +**Pods sur cette config :** +- 4× NestJS backend (750m CPU / 768Mi RAM) +- 2× Next.js frontend (500m CPU / 512Mi RAM) +- HPA actif : scale jusqu'à 8 pods NestJS + +--- + +### Palier Scale — 10 000 utilisateurs + +**Objectif :** Haute disponibilité, performances sous charge. + +``` +Control Plane : 3× CX22 (cluster etcd HA) — €5.11/mois × 3 = €15.33/mois +Workers : 6× CX52 (16 vCPU, 32 GB) — €43.49/mois × 6 = €260.94/mois + (autoscaling jusqu'à 12 nodes) +Load Balancer : LB31 (150 targets, 3 TB) — €39.15/mois +PostgreSQL : CCX33 primary (8 vCPU dédié) — €51.99/mois + + CCX23 replica (4 vCPU dédié) — €23.99/mois + + 500 GB Volume NVMe — €28.50/mois +Redis : CX42 dédié — €21.49/mois +Object Storage: ~€15/mois (extra 3 TB) +────────────────────────────────────────────────────── +TOTAL : ~€457/mois +``` + +--- + +## Localisation des serveurs (datacenter) + +Hetzner a des datacenters en : +- `fsn1` — Falkenstein, Allemagne (recommandé) +- `nbg1` — Nuremberg, Allemagne +- `hel1` — Helsinki, Finlande (bon pour RGPD nordique) +- `ash` — Ashburn, USA (si clients américains prioritaires) +- `hil` — Hillsboro, USA + +**Recommandation :** `fsn1` (Falkenstein) pour les clients européens. Même région pour tous les nœuds pour minimiser la latence réseau interne. + +```bash +# Vérifier les datacenters disponibles dans une région +hcloud datacenter list +# ID NAME DESCRIPTION LOCATION +# 1 fsn1-dc3 Falkenstein 1 virtual DC 3 fsn1 +# 2 nbg1-dc3 Nuremberg 1 virtual DC 3 nbg1 +# 3 hel1-dc2 Helsinki 1 virtual DC 2 hel1 +# 4 ash-dc1 Ashburn, Virginia 1 virtual DC 1 ash +# 5 hil-dc1 Hillsboro, Oregon 1 virtual DC 1 hil +``` + +--- + +## Load Balancer — Choix du plan + +| Plan | Targets | Trafic inclus | Connexions | Prix/mois | +|---|---|---|---|---| +| **LB11** | 25 | 1 TB | 1 000 simultanées | €7.49 | +| **LB21** | 75 | 2 TB | 10 000 simultanées | €22.14 | +| **LB31** | 150 | 3 TB | 100 000 simultanées | ~€39 | + +- **100 users :** LB11 largement suffisant +- **1 000 users :** LB21 recommandé (WebSocket = connexions persistantes) +- **10 000 users :** LB31 nécessaire (10 000 connexions simultanées pour WS + HTTP) + +**Configuration WebSocket sur le LB :** +```yaml +# Dans hetzner-k3s config (géré automatiquement) +# Le LB Hetzner supporte les WebSockets nativement +# Sticky sessions : cookie_name=SERVERID +``` + +--- + +## Décision finale : ARM64 ou x86 ? + +| Critère | x86 (CX) | ARM64 (CAX) | +|---|---|---| +| Coût | Référence | **-35%** | +| Performances Node.js | Bonne | **Équivalente ou meilleure** | +| Docker images officielles | ✅ linux/amd64 | ✅ linux/arm64 (Node.js 20, Alpine, etc.) | +| Build CI/CD | Simple | Nécessite `linux/arm64` ou multi-arch | +| Dépendances natives | Toutes supportées | 99% supportées (vérifier pdfkit, argon2) | +| Maturité | Très mature | Mature (2023+) | + +**Verdict pour Xpeditis :** Les CAX sont recommandées si vous acceptez la légère complexité de build multi-arch. Sinon, CX pour la simplicité. Ce guide utilise CX par défaut. + +> **Vérification des dépendances natives pour ARM64 :** +> - `argon2` → ✅ binaires précompilés ARM64 via `@node-rs/argon2` +> - `pdfkit` → ✅ pur JavaScript, pas de binaires natifs +> - `sharp` (si utilisé pour images) → ✅ binaires ARM64 disponibles +> - `better-sqlite3` → Non utilisé (TypeORM PG) diff --git a/docs/deployment/hetzner/05-k3s-cluster.md b/docs/deployment/hetzner/05-k3s-cluster.md new file mode 100644 index 0000000..ab0efdc --- /dev/null +++ b/docs/deployment/hetzner/05-k3s-cluster.md @@ -0,0 +1,476 @@ +# 05 — Création du cluster k3s avec hetzner-k3s + +C'est le fichier central. Suivez chaque étape dans l'ordre. + +--- + +## Qu'est-ce que hetzner-k3s ? + +[hetzner-k3s](https://github.com/vitobotta/hetzner-k3s) est un outil CLI qui automatise la création d'un cluster k3s sur Hetzner Cloud. En une commande, il : + +1. Crée les serveurs (control plane + workers) +2. Configure le réseau privé +3. Installe k3s sur tous les nœuds +4. Installe le Hetzner Cloud Controller Manager (provisionne LB + volumes depuis K8s) +5. Installe le Hetzner CSI Driver (PersistentVolumes sur Hetzner Volumes) +6. Configure le Cluster Autoscaler (scale automatique des workers) +7. Installe le System Upgrade Controller (upgrades k3s automatiques) +8. Configure kubectl localement + +--- + +## Fichier de configuration du cluster + +Créez le fichier `cluster.yaml` à la racine du projet ou dans un dossier sécurisé (jamais dans le repo Git) : + +```bash +mkdir -p ~/.xpeditis +cat > ~/.xpeditis/cluster.yaml << 'EOF' +# ============================================================ +# Xpeditis Production Cluster — hetzner-k3s configuration +# ============================================================ + +# Token API Hetzner (garder secret) +hetzner_token: "" + +# Nom du cluster +cluster_name: xpeditis-prod + +# Chemin du kubeconfig qui sera généré +kubeconfig_path: "~/.kube/kubeconfig-xpeditis-prod" + +# Version k3s +# Vérifier la dernière stable sur https://github.com/k3s-io/k3s/releases +k3s_version: v1.30.4+k3s1 + +# Clés SSH +public_ssh_key_path: "~/.ssh/xpeditis_hetzner.pub" +private_ssh_key_path: "~/.ssh/xpeditis_hetzner" +use_ssh_agent: false +ssh_port: 22 + +# Réseaux autorisés pour SSH et API Kubernetes +# Remplacer par votre IP fixe pour plus de sécurité +ssh_allowed_networks: + - "/32" + +api_allowed_networks: + - "/32" + +# Réseau privé Hetzner +# Créé dans le doc 03-hetzner-setup.md +existing_network: "xpeditis-network" +private_network_subnet: 10.0.0.0/16 + +# CIDRs Kubernetes (ne pas changer sauf conflit) +cluster_cidr: 10.244.0.0/16 +service_cidr: 10.96.0.0/16 +cluster_dns: 10.96.0.10 + +# Image OS +image: ubuntu-24.04 +snapshot_os: ubuntu + +# Datacenter (même région que l'Object Storage) +location: fsn1 + +# k3s options +disable_flannel: false # Flannel CNI (par défaut dans k3s) +schedule_workloads_on_masters: false # Masters dédiés au control plane + +# Packages additionnels installés sur chaque nœud +additional_packages: + - curl + - jq + - htop + - fail2ban # Protection brute force SSH + +# Commandes post-création sur chaque nœud +post_create_commands: + - apt-get update -qq + - apt-get install -y -qq fail2ban + - systemctl enable fail2ban + - systemctl start fail2ban + - | + cat >> /etc/fail2ban/jail.local << 'FAIL2BAN' + [sshd] + enabled = true + maxretry = 3 + bantime = 3600 + FAIL2BAN + - systemctl restart fail2ban + +# Helm charts installés automatiquement +cloud_controller_manager_manifest_url: "https://github.com/hetznercloud/hcloud-cloud-controller-manager/releases/download/v1.21.0/ccm-networks.yaml" +csi_driver_manifest_url: "https://raw.githubusercontent.com/hetznercloud/csi-driver/v2.8.0/deploy/kubernetes/hcloud-csi.yml" + +# System Upgrade Controller (upgrades k3s automatiques) +system_upgrade_controller_install: true +system_upgrade_controller_manifest_url: "https://github.com/rancher/system-upgrade-controller/releases/download/v0.13.4/system-upgrade-controller.yaml" + +# Cluster Autoscaler +cluster_autoscaler_install: true +cluster_autoscaler_version: "9.36.0" +cluster_autoscaler_image: "registry.k8s.io/autoscaling/cluster-autoscaler" +cluster_autoscaler_cmdline_args: + - --scan-interval=10s + - --scale-down-delay-after-add=5m + - --scale-down-unneeded-time=5m + - --max-nodes-total=12 + +# Metrics Server (pour HPA) +metrics_server_manifest_url: "https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml" + +# kube-apiserver extra args (sécurité) +kube_api_server_args: + - "--audit-log-path=/var/log/kubernetes/audit.log" + - "--audit-log-maxage=30" + - "--audit-log-maxbackup=3" + - "--audit-log-maxsize=100" + +# kubelet extra args +kubelet_args: + - "--max-pods=110" + - "--system-reserved=cpu=200m,memory=200Mi" + - "--kube-reserved=cpu=200m,memory=200Mi" + +# ============================================================ +# CONTROL PLANE +# ============================================================ +masters: + instance_type: cx22 # 2 vCPU, 4 GB + instance_count: 1 # Passer à 3 pour HA (10 000 users) + location: fsn1 + image: ~ # Utilise l'image globale + +# ============================================================ +# WORKER NODE POOLS +# ============================================================ +worker_node_pools: + - name: app-workers + instance_type: cx32 # 4 vCPU, 8 GB (MVP) + instance_count: 2 # Min pods + location: fsn1 + image: ~ + additional_packages: ~ + post_create_commands: ~ + taints: [] + labels: + - "xpeditis.io/node-role=app" + autoscaling: + enabled: true + min_instances: 2 # Minimum pour HA + max_instances: 6 # Max pour limiter les coûts +EOF +``` + +> **Pour le palier 1 000 users**, changez `cx32` → `cx42` et `max_instances: 8` +> **Pour le palier 10 000 users**, changez `cx42` → `cx52`, `instance_count: 4`, `max_instances: 12`, et `masters.instance_count: 3` + +--- + +## Création du cluster + +```bash +# Vérifier la configuration +hetzner-k3s validate --config ~/.xpeditis/cluster.yaml + +# Créer le cluster (prend 5-10 minutes) +hetzner-k3s create --config ~/.xpeditis/cluster.yaml + +# Output attendu : +# Creating infrastructure... +# Creating network... +# Creating SSH key... +# Creating firewall... +# Creating placement group... +# Creating load balancer... +# Creating masters... +# Waiting for masters to be ready... +# Creating worker pools... +# Waiting for workers to be ready... +# Installing k3s on masters... +# Installing k3s on workers... +# Installing Hetzner CCM... +# Installing Hetzner CSI... +# Installing Cluster Autoscaler... +# Installing System Upgrade Controller... +# Installing Metrics Server... +# Configuring kubeconfig... +# ✅ Cluster xpeditis-prod created successfully! +``` + +--- + +## Configuration de kubectl + +```bash +# Définir le KUBECONFIG +export KUBECONFIG=~/.kube/kubeconfig-xpeditis-prod + +# Ajouter au .zshrc ou .bashrc pour persistance +echo 'export KUBECONFIG=~/.kube/kubeconfig-xpeditis-prod' >> ~/.zshrc + +# Vérifier la connexion au cluster +kubectl cluster-info +# Kubernetes control plane is running at https://:6443 +# CoreDNS is running at https://:6443/api/v1/... + +# Lister les nœuds +kubectl get nodes -o wide +# NAME STATUS ROLES AGE VERSION +# xpeditis-prod-cx22-master-1 Ready control-plane,master 5m v1.30.4+k3s1 +# xpeditis-prod-cx32-worker-1 Ready 4m v1.30.4+k3s1 +# xpeditis-prod-cx32-worker-2 Ready 4m v1.30.4+k3s1 + +# Vérifier tous les pods système +kubectl get pods --all-namespaces +# Tous les pods doivent être Running +``` + +--- + +## Vérification du Hetzner Cloud Controller Manager + +Le CCM permet à Kubernetes de provisionner des ressources Hetzner (LB, volumes) : + +```bash +# Vérifier que le CCM tourne +kubectl get pods -n kube-system | grep hcloud + +# Vérifier que les nœuds ont le label de région +kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.topology\.kubernetes\.io/zone}{"\n"}{end}' +# xpeditis-prod-cx22-master-1 fsn1 +# xpeditis-prod-cx32-worker-1 fsn1 +# xpeditis-prod-cx32-worker-2 fsn1 +``` + +--- + +## Vérification du Hetzner CSI Driver + +```bash +# Le CSI driver permet de créer des PersistentVolumes sur Hetzner +kubectl get pods -n kube-system | grep hcloud-csi + +# Vérifier les StorageClasses disponibles +kubectl get storageclass +# NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE +# hcloud-volumes (default) csi.hetzner.cloud Delete WaitForFirstConsumer +``` + +--- + +## Configuration de Traefik (Ingress Controller) + +k3s installe Traefik par défaut. Nous devons le configurer pour : +1. Redirection HTTP → HTTPS +2. Support WebSocket (Socket.IO) +3. Sticky sessions pour le backend + +```bash +# Créer le fichier de configuration Traefik +cat > /tmp/traefik-config.yaml << 'EOF' +apiVersion: helm.cattle.io/v1 +kind: HelmChartConfig +metadata: + name: traefik + namespace: kube-system +spec: + valuesContent: |- + # Logs + logs: + general: + level: INFO + access: + enabled: true + + # Ports + ports: + web: + port: 8000 + redirectTo: + port: websecure # Force HTTPS + websecure: + port: 8443 + tls: + enabled: true + + # Sticky sessions pour WebSocket + service: + spec: + externalTrafficPolicy: Local + + # Annotations pour le Load Balancer Hetzner + service: + annotations: + load-balancer.hetzner.cloud/name: "xpeditis-lb" + load-balancer.hetzner.cloud/location: "fsn1" + load-balancer.hetzner.cloud/health-check-interval: "15s" + load-balancer.hetzner.cloud/health-check-timeout: "10s" + load-balancer.hetzner.cloud/health-check-retries: "3" + load-balancer.hetzner.cloud/use-private-ip: "true" + + # Ressources + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + + # Replicas (1 suffit pour MVP) + deployment: + replicas: 1 + + # Providers supplémentaires + providers: + kubernetesCRD: + enabled: true + allowCrossNamespace: true + kubernetesIngress: + enabled: true + publishedService: + enabled: true +EOF + +kubectl apply -f /tmp/traefik-config.yaml + +# Attendre que Traefik soit mis à jour +kubectl rollout status deployment/traefik -n kube-system --timeout=120s +``` + +--- + +## Installation de cert-manager + +cert-manager gère les certificats TLS automatiquement via Let's Encrypt : + +```bash +# Ajouter le repo Helm cert-manager +helm repo add jetstack https://charts.jetstack.io +helm repo update + +# Installer cert-manager +helm install cert-manager jetstack/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --version v1.15.3 \ + --set installCRDs=true \ + --set resources.requests.cpu=50m \ + --set resources.requests.memory=64Mi \ + --set webhook.resources.requests.cpu=50m \ + --set webhook.resources.requests.memory=32Mi + +# Attendre que cert-manager soit prêt +kubectl wait --for=condition=Ready pod \ + --selector=app.kubernetes.io/instance=cert-manager \ + -n cert-manager \ + --timeout=120s + +# Vérification +kubectl get pods -n cert-manager +# NAME READY STATUS +# cert-manager-7f9f87595d-xxx 1/1 Running +# cert-manager-cainjector-54db9f97d8-xxx 1/1 Running +# cert-manager-webhook-8698c586b7-xxx 1/1 Running +``` + +--- + +## ClusterIssuers Let's Encrypt + +```bash +# Créer les issuers (staging pour test, prod pour production) +cat > /tmp/cluster-issuers.yaml << 'EOF' +--- +# STAGING — Pour tester sans risquer le rate limit +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging +spec: + acme: + server: https://acme-staging-v02.api.letsencrypt.org/directory + email: admin@xpeditis.com # ← Remplacer + privateKeySecretRef: + name: letsencrypt-staging-key + solvers: + - http01: + ingress: + class: traefik +--- +# PRODUCTION — Certificats réels (max 5 renouvellements/semaine) +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: admin@xpeditis.com # ← Remplacer + privateKeySecretRef: + name: letsencrypt-prod-key + solvers: + - http01: + ingress: + class: traefik +EOF + +kubectl apply -f /tmp/cluster-issuers.yaml + +# Vérifier les issuers +kubectl get clusterissuers +# NAME READY AGE +# letsencrypt-staging True 30s +# letsencrypt-prod True 30s +``` + +--- + +## Récapitulatif : état du cluster après cette étape + +```bash +# Vue d'ensemble complète +kubectl get nodes +kubectl get pods --all-namespaces --field-selector=status.phase!=Running + +# Doit afficher : +# kube-system traefik-* Running +# kube-system hcloud-cloud-controller Running +# kube-system hcloud-csi-* Running +# kube-system coredns-* Running +# kube-system metrics-server-* Running +# cert-manager cert-manager-* Running + +echo "✅ Cluster prêt pour le déploiement de l'application" +``` + +--- + +## Opérations sur le cluster + +### Ajouter un nœud worker manuellement + +```bash +# Modifier le fichier cluster.yaml +# Changer instance_count de 2 → 3 dans worker_node_pools +hetzner-k3s apply --config ~/.xpeditis/cluster.yaml +``` + +### Supprimer le cluster (⚠️ irréversible) + +```bash +hetzner-k3s delete --config ~/.xpeditis/cluster.yaml +``` + +### Lister les composants Hetzner créés + +```bash +hcloud server list +hcloud load-balancer list +hcloud network list +hcloud firewall list +hcloud placement-group list +``` diff --git a/docs/deployment/hetzner/06-storage-s3.md b/docs/deployment/hetzner/06-storage-s3.md new file mode 100644 index 0000000..0228130 --- /dev/null +++ b/docs/deployment/hetzner/06-storage-s3.md @@ -0,0 +1,258 @@ +# 06 — Stockage objet S3 (Hetzner Object Storage) + +--- + +## Migration MinIO → Hetzner Object Storage + +Bonne nouvelle : **aucune modification de code nécessaire.** + +Le code Xpeditis utilise déjà le AWS SDK v3 avec `forcePathStyle: true` et un endpoint configurable dans `apps/backend/src/infrastructure/storage/s3-storage.adapter.ts` : + +```typescript +// Ce code existant fonctionne avec Hetzner Object Storage +this.s3Client = new S3Client({ + region, + endpoint, // ← Changer vers Hetzner + credentials: { accessKeyId, secretAccessKey }, + forcePathStyle: !!endpoint, // ← true pour Hetzner (path-style S3) +}); +``` + +Il suffit de **changer 4 variables d'environnement** : + +```bash +# AVANT (MinIO local) +AWS_S3_ENDPOINT=http://localhost:9000 +AWS_ACCESS_KEY_ID=minioadmin +AWS_SECRET_ACCESS_KEY=minioadmin +AWS_REGION=us-east-1 + +# APRÈS (Hetzner Object Storage) +AWS_S3_ENDPOINT=https://fsn1.your-objectstorage.com +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION=eu-central-1 +AWS_S3_BUCKET=xpeditis-prod +``` + +--- + +## Configuration détaillée + +### Variables d'environnement backend (.env.production) + +```bash +# S3 / Hetzner Object Storage +AWS_S3_ENDPOINT=https://fsn1.your-objectstorage.com +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION=eu-central-1 +AWS_S3_BUCKET=xpeditis-prod +``` + +> **Endpoint par région :** +> - Falkenstein : `https://fsn1.your-objectstorage.com` +> - Nuremberg : `https://nbg1.your-objectstorage.com` +> - Helsinki : `https://hel1.your-objectstorage.com` +> +> Utilisez la même région que vos serveurs pour éviter des frais de transfert inter-région. + +--- + +## Tester la connexion depuis le code + +```bash +# 1. Build l'image Docker backend avec les vars de test +# 2. Ou tester directement avec AWS CLI + +# Test avec AWS CLI (profil configuré dans doc 03) +aws s3 ls s3://xpeditis-prod/ \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com + +# Uploader un fichier test +echo "test" | aws s3 cp - s3://xpeditis-prod/test/health.txt \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com + +# Générer une URL signée (1h) +aws s3 presign s3://xpeditis-prod/test/health.txt \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com \ + --expires-in 3600 + +# Nettoyage +aws s3 rm s3://xpeditis-prod/test/health.txt \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com +``` + +--- + +## Structure du bucket + +Créez les "dossiers" initiaux (S3 utilise des préfixes, pas de vrais dossiers) : + +```bash +#!/bin/bash +PROFILE="hetzner" +ENDPOINT="https://fsn1.your-objectstorage.com" +BUCKET="xpeditis-prod" + +for PREFIX in documents pdfs exports logos backups/postgres; do + aws s3api put-object \ + --bucket "$BUCKET" \ + --key "$PREFIX/" \ + --profile "$PROFILE" \ + --endpoint-url "$ENDPOINT" \ + --content-length 0 + echo "✅ Créé: $PREFIX/" +done +``` + +--- + +## Lifecycle policies (économies de stockage) + +Hetzner Object Storage supporte les lifecycle rules S3 pour archiver automatiquement les anciens fichiers. + +```bash +# Créer le fichier de lifecycle +cat > /tmp/lifecycle.json << 'EOF' +{ + "Rules": [ + { + "ID": "archive-old-pdfs", + "Status": "Enabled", + "Filter": { + "Prefix": "pdfs/" + }, + "Transitions": [ + { + "Days": 90, + "StorageClass": "GLACIER" + } + ] + }, + { + "ID": "archive-old-exports", + "Status": "Enabled", + "Filter": { + "Prefix": "exports/" + }, + "Expiration": { + "Days": 365 + } + }, + { + "ID": "cleanup-old-backups", + "Status": "Enabled", + "Filter": { + "Prefix": "backups/" + }, + "Expiration": { + "Days": 30 + } + } + ] +} +EOF + +# Appliquer le lifecycle +aws s3api put-bucket-lifecycle-configuration \ + --bucket xpeditis-prod \ + --lifecycle-configuration file:///tmp/lifecycle.json \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com + +# Vérifier +aws s3api get-bucket-lifecycle-configuration \ + --bucket xpeditis-prod \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com +``` + +--- + +## CORS (pour upload direct depuis le navigateur) + +Si vous implémentez des uploads directs depuis le browser (carrier portal) : + +```bash +cat > /tmp/cors.json << 'EOF' +{ + "CORSRules": [ + { + "AllowedHeaders": ["*"], + "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"], + "AllowedOrigins": [ + "https://app.xpeditis.com", + "https://xpeditis.com" + ], + "ExposeHeaders": ["ETag"], + "MaxAgeSeconds": 3000 + } + ] +} +EOF + +aws s3api put-bucket-cors \ + --bucket xpeditis-prod \ + --cors-configuration file:///tmp/cors.json \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com +``` + +--- + +## Intégration dans le Secret Kubernetes + +Le Secret Kubernetes `backend-secrets` contiendra les credentials S3. Voir le doc 09 pour les manifests complets, mais voici la section S3 : + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: backend-secrets + namespace: xpeditis-prod +type: Opaque +stringData: + AWS_S3_ENDPOINT: "https://fsn1.your-objectstorage.com" + AWS_ACCESS_KEY_ID: "" + AWS_SECRET_ACCESS_KEY: "" + AWS_REGION: "eu-central-1" + AWS_S3_BUCKET: "xpeditis-prod" +``` + +--- + +## Monitoring du stockage + +```bash +# Voir la taille totale du bucket +aws s3 ls s3://xpeditis-prod/ \ + --recursive \ + --human-readable \ + --summarize \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com \ + | tail -3 + +# Output : +# Total Objects: 1234 +# Total Size: 4.5 GiB +``` + +--- + +## Tarifs Hetzner Object Storage + +| Ressource | Prix | +|---|---| +| Stockage | Inclus dans le pack €4.99/mois (1 TB) | +| Trafic sortant (internet) | Inclus dans le pack (1 TB) | +| Requêtes | Incluses | +| Stockage > 1 TB | €0.0067/TB/heure (~€4.90/TB/mois) | +| Trafic > 1 TB | ~€1/TB | + +Pour Xpeditis à 1 000 users (~200 GB de fichiers), le coût est de **€4.99/mois fixe**. diff --git a/docs/deployment/hetzner/07-database-postgresql.md b/docs/deployment/hetzner/07-database-postgresql.md new file mode 100644 index 0000000..3c18be9 --- /dev/null +++ b/docs/deployment/hetzner/07-database-postgresql.md @@ -0,0 +1,337 @@ +# 07 — Base de données PostgreSQL + +Deux options selon votre palier et votre tolérance aux opérations. + +--- + +## Option A — Neon.tech (recommandé pour MVP) + +### Pourquoi Neon.tech + +- PostgreSQL 15 managé, compatible TypeORM +- Extensions `uuid-ossp` et `pg_trgm` **disponibles** (requises par Xpeditis) +- Connection pooling intégré (PgBouncer) → critique pour NestJS multi-pods +- Backups automatiques + point-in-time recovery +- Free tier pour le développement +- **$19/mois** pour le plan Pro (production) +- Pas de gestion de HA, de réplication, ni de backups à faire + +### Setup Neon.tech + +1. Créez un compte sur https://neon.tech +2. "New Project" → Nom: `xpeditis-prod` → Region: `AWS eu-central-1 (Frankfurt)` (le plus proche de Hetzner FSN1) +3. Sélectionnez **Plan Pro** ($19/mois) +4. PostgreSQL version: **15** + +### Créer la base de données + +```bash +# Dans l'interface Neon → SQL Editor, ou via CLI neon +# Installer la CLI Neon +npm install -g neonctl +neonctl auth + +# Créer les extensions requises par Xpeditis +neonctl sql --project-id << 'EOF' +-- Extensions requises par Xpeditis +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + +-- Vérification +SELECT extname, extversion FROM pg_extension +WHERE extname IN ('uuid-ossp', 'pg_trgm'); +EOF +``` + +### Connection string + +Dans l'interface Neon → Connection Details → choisissez **Pooled connection** : + +```bash +# Connection string avec pooling (via PgBouncer) — pour la prod +postgresql://xpeditis:@ep-xxx-xxx.eu-central-1.aws.neon.tech/xpeditis?pgbouncer=true&connection_limit=1&sslmode=require + +# Connection string directe — pour les migrations TypeORM +postgresql://xpeditis:@ep-xxx-xxx.eu-central-1.aws.neon.tech/xpeditis?sslmode=require +``` + +> **Important :** TypeORM migrations doivent utiliser la **connexion directe** (sans pgbouncer). Pour le runtime NestJS, utilisez la **connexion poolée**. + +### Configuration TypeORM pour Neon + +L'app utilise des variables séparées pour l'hôte/port. Modifiez pour utiliser `DATABASE_URL` : + +Vérifiez le fichier `apps/backend/src/app.module.ts`. Si TypeORM est configuré avec des variables séparées (`DATABASE_HOST`, `DATABASE_PORT`, etc.), vous avez deux options : + +**Option 1 (recommandée) — URL complète :** + +Dans le `app.module.ts`, TypeOrmModule accepte une `url` : +```typescript +TypeOrmModule.forRootAsync({ + useFactory: (configService: ConfigService) => ({ + type: 'postgres', + url: configService.get('DATABASE_URL'), // ← Utiliser si disponible + ssl: { rejectUnauthorized: false }, // ← Requis pour Neon + // ... reste de la config + }), +}) +``` + +**Option 2 — Variables séparées (configuration actuelle) :** + +Décomposez l'URL Neon en variables séparées dans le `.env` : +```bash +DATABASE_HOST=ep-xxx-xxx.eu-central-1.aws.neon.tech +DATABASE_PORT=5432 +DATABASE_USER=xpeditis +DATABASE_PASSWORD= +DATABASE_NAME=xpeditis +DATABASE_SSL=true +``` + +Et ajoutez `ssl: { rejectUnauthorized: false }` dans la config TypeORM. + +### Lancer les migrations + +```bash +# Se placer dans le répertoire backend +cd apps/backend + +# Copier l'env de prod +cp .env.example .env.production + +# Éditer .env.production avec les vraies valeurs Neon +# DATABASE_HOST=ep-xxx-xxx.eu-central-1.aws.neon.tech +# DATABASE_USER=xpeditis +# DATABASE_PASSWORD= +# DATABASE_NAME=xpeditis +# DATABASE_SSL=true + +# Lancer les migrations (connexion directe, pas poolée) +NODE_ENV=production npm run migration:run + +# Vérifier les migrations appliquées +NODE_ENV=production npm run typeorm query "SELECT version, name FROM typeorm_migrations ORDER BY id" +``` + +--- + +## Option B — PostgreSQL self-hosted sur Hetzner (1 000+ users) + +### Architecture recommandée + +``` +CX22/CCX13 ─── PostgreSQL primary (lecture + écriture) + │ +CCX13 ─── PostgreSQL replica (lecture seule + failover) + │ +Volume Hetzner ─── /var/lib/postgresql/data (persistant) +``` + +### 1. Créer le serveur PostgreSQL dédié + +```bash +# Créer un serveur CCX13 dédié pour PostgreSQL +hcloud server create \ + --name xpeditis-postgres \ + --type ccx13 \ + --image ubuntu-24.04 \ + --location fsn1 \ + --ssh-key xpeditis-deploy \ + --network xpeditis-network \ + --firewall xpeditis-firewall + +# Attacher le volume de données +hcloud volume attach xpeditis-postgres-data \ + --server xpeditis-postgres \ + --automount + +# Récupérer l'IP privée +POSTGRES_PRIVATE_IP=$(hcloud server ip xpeditis-postgres --private-ip) +echo "PostgreSQL IP privée: $POSTGRES_PRIVATE_IP" +``` + +### 2. Installer et configurer PostgreSQL + +```bash +# Se connecter au serveur +ssh -i ~/.ssh/xpeditis_hetzner root@ + +# Installer PostgreSQL 15 +apt-get update +apt-get install -y postgresql-15 postgresql-client-15 + +# Monter le volume de données +DEVICE_NAME=$(lsblk -o NAME,SERIAL | grep HC | head -1 | awk '{print $1}') +mkfs.ext4 /dev/$DEVICE_NAME +mkdir -p /mnt/postgres-data +mount /dev/$DEVICE_NAME /mnt/postgres-data +echo "/dev/$DEVICE_NAME /mnt/postgres-data ext4 defaults 0 2" >> /etc/fstab + +# Déplacer les données PostgreSQL vers le volume +systemctl stop postgresql +rsync -av /var/lib/postgresql /mnt/postgres-data/ +rm -rf /var/lib/postgresql/15/main +ln -s /mnt/postgres-data/postgresql/15/main /var/lib/postgresql/15/main +systemctl start postgresql + +# Créer la base de données et l'utilisateur +sudo -u postgres psql << 'PGSQL' +CREATE USER xpeditis WITH PASSWORD ''; +CREATE DATABASE xpeditis_prod OWNER xpeditis; +GRANT ALL PRIVILEGES ON DATABASE xpeditis_prod TO xpeditis; + +-- Connecter à la base +\c xpeditis_prod + +-- Extensions requises +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + +-- Vérification +SELECT extname FROM pg_extension; +PGSQL +``` + +### 3. Configuration PostgreSQL pour la production + +```bash +# Éditer /etc/postgresql/15/main/postgresql.conf +cat >> /etc/postgresql/15/main/postgresql.conf << 'EOF' + +# Performance tuning (pour CCX13 : 4 vCPU dédiés, 8 GB RAM) +shared_buffers = 2GB # 25% de la RAM +effective_cache_size = 6GB # 75% de la RAM +maintenance_work_mem = 512MB +checkpoint_completion_target = 0.9 +wal_buffers = 16MB +default_statistics_target = 100 +random_page_cost = 1.1 # SSD NVMe +effective_io_concurrency = 200 # SSD +work_mem = 64MB +min_wal_size = 1GB +max_wal_size = 4GB + +# Connexions +max_connections = 100 +# Avec RDS Proxy / PgBouncer en front, 100 suffisent + +# Logging +log_destination = 'stderr' +logging_collector = on +log_directory = '/var/log/postgresql' +log_filename = 'postgresql-%Y-%m-%d.log' +log_rotation_age = 1d +log_min_duration_statement = 1000 # Log les queries > 1s +log_checkpoints = on +log_connections = on +log_disconnections = on +log_lock_waits = on + +# Réplication (pour replica future) +wal_level = replica +max_wal_senders = 3 +max_replication_slots = 3 +EOF + +# Autoriser les connexions depuis le réseau privé Hetzner +cat >> /etc/postgresql/15/main/pg_hba.conf << 'EOF' + +# Connexions depuis le réseau privé Hetzner (pods k3s) +host xpeditis_prod xpeditis 10.0.0.0/16 md5 +EOF + +# Écouter sur toutes les interfaces (nécessaire pour le réseau privé) +sed -i "s/#listen_addresses = 'localhost'/listen_addresses = '10.0.0.0\/16,localhost'/" \ + /etc/postgresql/15/main/postgresql.conf + +systemctl restart postgresql +systemctl enable postgresql + +# Test de connexion depuis le réseau privé +psql -h $POSTGRES_PRIVATE_IP -U xpeditis -d xpeditis_prod -c "SELECT version();" +``` + +### 4. Installer PgBouncer (connection pooling) + +NestJS crée une connexion par pod. Sans pooler, 10 pods × 10 connexions = 100 connexions constantes. PgBouncer réduit ça drastiquement. + +```bash +apt-get install -y pgbouncer + +cat > /etc/pgbouncer/pgbouncer.ini << 'EOF' +[databases] +xpeditis_prod = host=localhost port=5432 dbname=xpeditis_prod + +[pgbouncer] +listen_addr = 0.0.0.0 +listen_port = 6432 +auth_type = md5 +auth_file = /etc/pgbouncer/userlist.txt +pool_mode = transaction # Mode le plus efficace pour NestJS +max_client_conn = 500 # Connexions clients max +default_pool_size = 20 # Connexions vers PostgreSQL par pool +reserve_pool_size = 5 +server_reset_query = DISCARD ALL +log_connections = 1 +log_disconnections = 1 +logfile = /var/log/pgbouncer/pgbouncer.log +pidfile = /var/run/pgbouncer/pgbouncer.pid +EOF + +# Créer le fichier d'authentification +echo '"xpeditis" "md5"' > /etc/pgbouncer/userlist.txt +# Pour générer le hash md5 : +echo -n "md5$(echo -n 'xpeditis' | md5sum | awk '{print $1}')" + +systemctl enable pgbouncer +systemctl start pgbouncer +``` + +Avec PgBouncer, les pods NestJS se connectent sur le port `6432` : +```bash +# Variables d'environnement pour PgBouncer +DATABASE_HOST= +DATABASE_PORT=6432 # PgBouncer au lieu de 5432 +``` + +### 5. Lancer les migrations TypeORM + +```bash +# Depuis votre machine locale (ou depuis un pod de migration) +cd apps/backend +DATABASE_HOST= \ +DATABASE_PORT=5432 \ # Direct PostgreSQL pour les migrations (pas PgBouncer) +DATABASE_USER=xpeditis \ +DATABASE_PASSWORD= \ +DATABASE_NAME=xpeditis_prod \ +npm run migration:run + +# Vérifier +DATABASE_HOST= \ +DATABASE_PORT=5432 \ +DATABASE_USER=xpeditis \ +DATABASE_PASSWORD= \ +DATABASE_NAME=xpeditis_prod \ +npm run typeorm query "SELECT COUNT(*) as migrations FROM typeorm_migrations" +``` + +--- + +## Comparaison des options + +| Critère | Neon.tech (Option A) | Self-hosted (Option B) | +|---|---|---| +| **Coût (1 000 users)** | $19/mois | ~€30/mois (CCX13 + volume) | +| **HA** | Automatique | Manuel (Patroni) | +| **Backups** | Automatique (7 jours PITR) | Script cron (doc 13) | +| **Extensions** | uuid-ossp + pg_trgm ✅ | Toutes | +| **Migrations** | Simple | Simple | +| **Ops requis** | Aucun | Maintenance mensuelle | +| **Scale** | Jusqu'à $69/mois (Pro) | Changement de serveur | +| **Limite connexions** | PgBouncer inclus | PgBouncer à installer | + +**Recommandation :** +- < 500 users → **Neon.tech** (aucun ops, $19/mois) +- 500–5 000 users → **Self-hosted CCX23** (plus économique à ce niveau) +- > 5 000 users → **Self-hosted CCX33 + replica** (contrôle total) diff --git a/docs/deployment/hetzner/08-redis-setup.md b/docs/deployment/hetzner/08-redis-setup.md new file mode 100644 index 0000000..2d235b8 --- /dev/null +++ b/docs/deployment/hetzner/08-redis-setup.md @@ -0,0 +1,313 @@ +# 08 — Redis Setup + +Redis est utilisé dans Xpeditis pour : +1. **Cache des rate quotes** — clés `rate:{origin}:{destination}:{containerType}`, TTL 15 min +2. **Pub/sub WebSocket** — Socket.IO multi-pods nécessite Redis pour broadcaster les notifications + +--- + +## Option A — Upstash (recommandé pour MVP) + +### Pourquoi Upstash + +- Redis serverless, pay-per-use ($0.2 per 100K commands) +- Free tier : 10 000 commandes/jour, 256 MB (suffisant pour 100 users) +- Compatible avec l'interface Redis standard (ioredis) +- Support TLS natif +- Régions EU disponibles (Frankfurt) +- **Pas de serveur à gérer** + +### Setup Upstash + +1. Créez un compte sur https://upstash.com +2. **Create Database** + - Name: `xpeditis-prod` + - Region: `EU-WEST-1 (Frankfurt)` ← le plus proche de Hetzner FSN1 + - Type: **Regional** (pas Global pour commencer) + - Eviction: **Allkeys-LRU** (expire les clés les plus anciennes si mémoire pleine) + - TLS: **Enabled** +3. Copiez les credentials affichés + +### Variables d'environnement + +```bash +# Upstash fournit une URL Redis complète +REDIS_HOST=your-redis.upstash.io +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# OU avec URL (si le code le supporte) +REDIS_URL=redis://:password@your-redis.upstash.io:6379 +``` + +### Vérification de la connexion + +```bash +# Test avec redis-cli +redis-cli -h your-redis.upstash.io -p 6379 -a --tls ping +# PONG + +# Test de set/get +redis-cli -h your-redis.upstash.io -p 6379 -a --tls \ + SET test:connection "xpeditis-ok" EX 60 +redis-cli -h your-redis.upstash.io -p 6379 -a --tls \ + GET test:connection +# "xpeditis-ok" +``` + +### Configuration dans l'app Xpeditis + +Le code NestJS utilise `ioredis`. Vérifiez que TLS est activé dans la config cache : + +Dans `apps/backend/src/infrastructure/cache/cache.module.ts`, assurez-vous que la config Redis accepte TLS : + +```typescript +// La config doit inclure TLS pour Upstash +const redisOptions = { + host: configService.get('REDIS_HOST'), + port: configService.get('REDIS_PORT', 6379), + password: configService.get('REDIS_PASSWORD'), + db: configService.get('REDIS_DB', 0), + // TLS requis pour Upstash + tls: configService.get('NODE_ENV') === 'production' ? {} : undefined, +}; +``` + +> Si la config actuelle ne supporte pas TLS, ajoutez la variable `REDIS_TLS=true` et adaptez le cache module en conséquence. + +--- + +## Option B — Redis self-hosted dans k3s + +### Quand choisir cette option + +- 1 000+ users (le free tier Upstash devient limité) +- Besoin de Redis Cluster pour le WebSocket à grande échelle +- Contrôle total des données + +### StatefulSet Redis dans Kubernetes + +```bash +# Créer le namespace si pas encore fait +kubectl create namespace xpeditis-prod 2>/dev/null || true + +# Créer le Secret Redis +cat > /tmp/redis-secret.yaml << 'EOF' +apiVersion: v1 +kind: Secret +metadata: + name: redis-secret + namespace: xpeditis-prod +type: Opaque +stringData: + REDIS_PASSWORD: "" +EOF +kubectl apply -f /tmp/redis-secret.yaml + +# Créer la ConfigMap Redis +cat > /tmp/redis-config.yaml << 'EOF' +apiVersion: v1 +kind: ConfigMap +metadata: + name: redis-config + namespace: xpeditis-prod +data: + redis.conf: | + # Sécurité + requirepass + protected-mode yes + + # Persistance (AOF pour durabilité) + appendonly yes + appendfsync everysec + auto-aof-rewrite-percentage 100 + auto-aof-rewrite-min-size 64mb + + # Mémoire + maxmemory 512mb + maxmemory-policy allkeys-lru + + # Réseau + bind 0.0.0.0 + tcp-backlog 511 + timeout 0 + tcp-keepalive 300 + + # Logging + loglevel notice + + # Performances + lazyfree-lazy-eviction yes + lazyfree-lazy-expire yes +EOF +kubectl apply -f /tmp/redis-config.yaml + +# Créer le StatefulSet Redis +cat > /tmp/redis-statefulset.yaml << 'EOF' +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: redis + namespace: xpeditis-prod +spec: + serviceName: redis-headless + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:7-alpine + ports: + - containerPort: 6379 + name: redis + command: + - redis-server + - /etc/redis/redis.conf + env: + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: redis-secret + key: REDIS_PASSWORD + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + volumeMounts: + - name: redis-config-vol + mountPath: /etc/redis + - name: redis-data + mountPath: /data + readinessProbe: + exec: + command: + - redis-cli + - -a + - $(REDIS_PASSWORD) + - ping + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + exec: + command: + - redis-cli + - -a + - $(REDIS_PASSWORD) + - ping + initialDelaySeconds: 30 + periodSeconds: 30 + volumes: + - name: redis-config-vol + configMap: + name: redis-config + volumeClaimTemplates: + - metadata: + name: redis-data + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: hcloud-volumes + resources: + requests: + storage: 5Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: redis-headless + namespace: xpeditis-prod +spec: + clusterIP: None + selector: + app: redis + ports: + - port: 6379 + targetPort: 6379 +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: xpeditis-prod +spec: + selector: + app: redis + ports: + - port: 6379 + targetPort: 6379 + type: ClusterIP +EOF + +kubectl apply -f /tmp/redis-statefulset.yaml + +# Attendre que Redis soit prêt +kubectl rollout status statefulset/redis -n xpeditis-prod --timeout=120s + +# Tester Redis +kubectl exec -it redis-0 -n xpeditis-prod -- redis-cli -a ping +# PONG +``` + +### Variables d'environnement pour Redis self-hosted + +```bash +REDIS_HOST=redis.xpeditis-prod.svc.cluster.local +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 +# Pas de TLS (réseau privé interne k3s) +``` + +--- + +## Vérification du cache Redis dans Xpeditis + +Après déploiement de l'application, vérifiez que le cache fonctionne : + +```bash +# Se connecter à Redis +kubectl exec -it redis-0 -n xpeditis-prod -- redis-cli -a + +# Après quelques rate searches depuis l'app : +KEYS rate:* +# 1) "rate:FRNCE:DEHAM:20ft" +# 2) "rate:FRNCE:NLRTM:20ft" +# ... + +# Vérifier un TTL (doit être < 900 = 15 min) +TTL "rate:FRNCE:DEHAM:20ft" +# (integer) 647 + +# Stats globales +INFO stats +# keyspace_hits: 1234 +# keyspace_misses: 156 +# → Taux de hit = 1234/(1234+156) = 88% ✅ +``` + +--- + +## Comparaison des options + +| Critère | Upstash (Option A) | Self-hosted (Option B) | +|---|---|---| +| **Coût 100 users** | $0 (free tier) | ~€5/mois (stockage) | +| **Coût 1 000 users** | ~$5-10/mois | ~€5-10/mois | +| **Setup** | 5 minutes | 30 minutes | +| **HA** | Automatique | Non (StatefulSet 1 replica) | +| **TLS** | Forcé | Non (cluster interne) | +| **Ops** | Aucun | Monitoring mémoire | +| **Latence** | ~5-10ms (Frankfurt) | <1ms (cluster interne) | + +**Recommandation :** +- MVP → **Upstash free tier** (zéro coût, zéro ops) +- 1 000+ users → **Self-hosted dans k3s** (latence minimale, contrôle complet) diff --git a/docs/deployment/hetzner/09-kubernetes-manifests.md b/docs/deployment/hetzner/09-kubernetes-manifests.md new file mode 100644 index 0000000..2fa4dfe --- /dev/null +++ b/docs/deployment/hetzner/09-kubernetes-manifests.md @@ -0,0 +1,767 @@ +# 09 — Manifests Kubernetes complets + +Tous les fichiers YAML de déploiement de Xpeditis. Créez un dossier `k8s/` à la racine du projet. + +--- + +## Structure des fichiers + +``` +k8s/ +├── 00-namespaces.yaml +├── 01-secrets.yaml # ← À remplir avec vos valeurs (ne pas committer) +├── 02-configmaps.yaml +├── 03-backend-deployment.yaml +├── 04-backend-service.yaml +├── 05-frontend-deployment.yaml +├── 06-frontend-service.yaml +├── 07-ingress.yaml +├── 08-hpa.yaml +└── 09-pdb.yaml +``` + +--- + +## 00 — Namespaces + +```yaml +# k8s/00-namespaces.yaml +--- +apiVersion: v1 +kind: Namespace +metadata: + name: xpeditis-prod + labels: + environment: production + app.kubernetes.io/managed-by: hetzner-k3s +``` + +```bash +kubectl apply -f k8s/00-namespaces.yaml +``` + +--- + +## 01 — Secrets (⚠️ ne jamais committer ce fichier dans Git) + +Ajoutez `k8s/01-secrets.yaml` à votre `.gitignore`. + +```yaml +# k8s/01-secrets.yaml ← AJOUTER AU .gitignore +--- +apiVersion: v1 +kind: Secret +metadata: + name: backend-secrets + namespace: xpeditis-prod +type: Opaque +stringData: + # Application + NODE_ENV: "production" + PORT: "4000" + API_PREFIX: "api/v1" + APP_URL: "https://app.xpeditis.com" + FRONTEND_URL: "https://app.xpeditis.com" + + # Base de données (choisir Option A ou B) + # === Option A : Neon.tech === + DATABASE_HOST: "ep-xxx.eu-central-1.aws.neon.tech" + DATABASE_PORT: "5432" + DATABASE_USER: "xpeditis" + DATABASE_PASSWORD: "" + DATABASE_NAME: "xpeditis" + DATABASE_SSL: "true" + DATABASE_SYNC: "false" + DATABASE_LOGGING: "false" + # === Option B : Self-hosted === + # DATABASE_HOST: "10.0.1.100" # IP privée Hetzner du serveur PG + # DATABASE_PORT: "6432" # PgBouncer + # DATABASE_USER: "xpeditis" + # DATABASE_PASSWORD: "" + # DATABASE_NAME: "xpeditis_prod" + # DATABASE_SYNC: "false" + # DATABASE_LOGGING: "false" + + # Redis (choisir Option A ou B) + # === Option A : Upstash === + REDIS_HOST: "your-redis.upstash.io" + REDIS_PORT: "6379" + REDIS_PASSWORD: "" + REDIS_DB: "0" + # === Option B : Self-hosted === + # REDIS_HOST: "redis.xpeditis-prod.svc.cluster.local" + # REDIS_PORT: "6379" + # REDIS_PASSWORD: "" + # REDIS_DB: "0" + + # JWT + JWT_SECRET: "" + JWT_ACCESS_EXPIRATION: "15m" + JWT_REFRESH_EXPIRATION: "7d" + + # OAuth2 Google + GOOGLE_CLIENT_ID: "" + GOOGLE_CLIENT_SECRET: "" + GOOGLE_CALLBACK_URL: "https://api.xpeditis.com/api/v1/auth/google/callback" + + # OAuth2 Microsoft + MICROSOFT_CLIENT_ID: "" + MICROSOFT_CLIENT_SECRET: "" + MICROSOFT_CALLBACK_URL: "https://api.xpeditis.com/api/v1/auth/microsoft/callback" + + # Email (Brevo SMTP — remplace SendGrid) + SMTP_HOST: "smtp-relay.brevo.com" + SMTP_PORT: "587" + SMTP_SECURE: "false" + SMTP_USER: "" + SMTP_PASS: "" + SMTP_FROM: "noreply@xpeditis.com" + + # Hetzner Object Storage (remplace MinIO) + AWS_S3_ENDPOINT: "https://fsn1.your-objectstorage.com" + AWS_ACCESS_KEY_ID: "" + AWS_SECRET_ACCESS_KEY: "" + AWS_REGION: "eu-central-1" + AWS_S3_BUCKET: "xpeditis-prod" + + # Carrier APIs + MAERSK_API_KEY: "" + MAERSK_API_URL: "https://api.maersk.com/v1" + MSC_API_KEY: "" + MSC_API_URL: "https://api.msc.com/v1" + CMACGM_API_URL: "https://api.cma-cgm.com/v1" + CMACGM_CLIENT_ID: "" + CMACGM_CLIENT_SECRET: "" + HAPAG_API_URL: "https://api.hapag-lloyd.com/v1" + HAPAG_API_KEY: "" + ONE_API_URL: "https://api.one-line.com/v1" + ONE_USERNAME: "" + ONE_PASSWORD: "" + + # Stripe + STRIPE_SECRET_KEY: "sk_live_<...>" + STRIPE_WEBHOOK_SECRET: "whsec_<...>" + STRIPE_SILVER_MONTHLY_PRICE_ID: "price_<...>" + STRIPE_SILVER_YEARLY_PRICE_ID: "price_<...>" + STRIPE_GOLD_MONTHLY_PRICE_ID: "price_<...>" + STRIPE_GOLD_YEARLY_PRICE_ID: "price_<...>" + STRIPE_PLATINIUM_MONTHLY_PRICE_ID: "price_<...>" + STRIPE_PLATINIUM_YEARLY_PRICE_ID: "price_<...>" + + # Sécurité + BCRYPT_ROUNDS: "12" + SESSION_TIMEOUT_MS: "7200000" + RATE_LIMIT_TTL: "60" + RATE_LIMIT_MAX: "100" + + # Monitoring + SENTRY_DSN: "" +--- +apiVersion: v1 +kind: Secret +metadata: + name: frontend-secrets + namespace: xpeditis-prod +type: Opaque +stringData: + NEXT_PUBLIC_API_URL: "https://api.xpeditis.com" + NEXT_PUBLIC_APP_URL: "https://app.xpeditis.com" + NEXT_PUBLIC_API_PREFIX: "api/v1" + NEXTAUTH_URL: "https://app.xpeditis.com" + NEXTAUTH_SECRET: "" + GOOGLE_CLIENT_ID: "" + GOOGLE_CLIENT_SECRET: "" + MICROSOFT_CLIENT_ID: "" + MICROSOFT_CLIENT_SECRET: "" + NODE_ENV: "production" +``` + +```bash +# Générer les secrets aléatoires +echo "JWT_SECRET=$(openssl rand -base64 48)" +echo "NEXTAUTH_SECRET=$(openssl rand -base64 24)" + +# Appliquer (après avoir rempli les valeurs) +kubectl apply -f k8s/01-secrets.yaml + +# Vérifier (sans voir les valeurs) +kubectl get secret backend-secrets -n xpeditis-prod -o jsonpath='{.data}' | jq 'keys' +``` + +--- + +## 02 — ConfigMaps (variables non-sensibles) + +```yaml +# k8s/02-configmaps.yaml +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: backend-config + namespace: xpeditis-prod +data: + # Ces valeurs ne sont pas sensibles + LOG_LEVEL: "info" + TZ: "Europe/Paris" + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: frontend-config + namespace: xpeditis-prod +data: + TZ: "Europe/Paris" +``` + +--- + +## 03 — Deployment Backend NestJS + +```yaml +# k8s/03-backend-deployment.yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: xpeditis-backend + namespace: xpeditis-prod + labels: + app: xpeditis-backend + version: "latest" +spec: + replicas: 2 + selector: + matchLabels: + app: xpeditis-backend + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 # Zero downtime deployment + template: + metadata: + labels: + app: xpeditis-backend + version: "latest" + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "4000" + prometheus.io/path: "/api/v1/health" + spec: + # Anti-affinité : pods sur nœuds différents + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - xpeditis-backend + topologyKey: kubernetes.io/hostname + + # Temps de grâce pour les connexions WebSocket + terminationGracePeriodSeconds: 60 + + containers: + - name: backend + # L'image est mise à jour par le CI/CD (doc 11) + image: ghcr.io//xpeditis-backend:latest + imagePullPolicy: Always + ports: + - containerPort: 4000 + name: http + protocol: TCP + + # Variables d'environnement depuis les Secrets + envFrom: + - secretRef: + name: backend-secrets + - configMapRef: + name: backend-config + + # Resources (MVP — ajuster selon les métriques réelles) + resources: + requests: + cpu: "500m" + memory: "512Mi" + limits: + cpu: "2000m" + memory: "1.5Gi" + + # Health checks + startupProbe: + httpGet: + path: /api/v1/health + port: 4000 + initialDelaySeconds: 20 + periodSeconds: 5 + failureThreshold: 12 # 60 secondes max au démarrage + + readinessProbe: + httpGet: + path: /api/v1/health + port: 4000 + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + + livenessProbe: + httpGet: + path: /api/v1/health + port: 4000 + initialDelaySeconds: 60 + periodSeconds: 30 + failureThreshold: 3 + + # Lifecycle hook pour graceful shutdown + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "sleep 10"] # Laisse le temps au LB de retirer le pod + + # Pull depuis GHCR (GitHub Container Registry) + imagePullSecrets: + - name: ghcr-credentials + + # Redémarrage automatique + restartPolicy: Always +``` + +--- + +## 04 — Service Backend + +```yaml +# k8s/04-backend-service.yaml +--- +apiVersion: v1 +kind: Service +metadata: + name: xpeditis-backend + namespace: xpeditis-prod + labels: + app: xpeditis-backend +spec: + selector: + app: xpeditis-backend + ports: + - name: http + port: 4000 + targetPort: 4000 + protocol: TCP + type: ClusterIP +``` + +--- + +## 05 — Deployment Frontend Next.js + +```yaml +# k8s/05-frontend-deployment.yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: xpeditis-frontend + namespace: xpeditis-prod + labels: + app: xpeditis-frontend +spec: + replicas: 1 + selector: + matchLabels: + app: xpeditis-frontend + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: xpeditis-frontend + spec: + terminationGracePeriodSeconds: 30 + + containers: + - name: frontend + image: ghcr.io//xpeditis-frontend:latest + imagePullPolicy: Always + ports: + - containerPort: 3000 + name: http + + envFrom: + - secretRef: + name: frontend-secrets + - configMapRef: + name: frontend-config + + resources: + requests: + cpu: "250m" + memory: "256Mi" + limits: + cpu: "1000m" + memory: "768Mi" + + startupProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 12 + + readinessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + + livenessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 30 + failureThreshold: 3 + + lifecycle: + preStop: + exec: + command: ["/bin/sh", "-c", "sleep 5"] + + imagePullSecrets: + - name: ghcr-credentials + + restartPolicy: Always +``` + +--- + +## 06 — Service Frontend + +```yaml +# k8s/06-frontend-service.yaml +--- +apiVersion: v1 +kind: Service +metadata: + name: xpeditis-frontend + namespace: xpeditis-prod + labels: + app: xpeditis-frontend +spec: + selector: + app: xpeditis-frontend + ports: + - name: http + port: 3000 + targetPort: 3000 + protocol: TCP + type: ClusterIP +``` + +--- + +## 07 — Ingress (Traefik + TLS) + +```yaml +# k8s/07-ingress.yaml +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: xpeditis-ingress + namespace: xpeditis-prod + annotations: + # TLS via cert-manager + cert-manager.io/cluster-issuer: "letsencrypt-prod" + + # Traefik config + traefik.ingress.kubernetes.io/router.entrypoints: "websecure" + traefik.ingress.kubernetes.io/router.tls: "true" + + # Sticky sessions pour WebSocket Socket.IO + traefik.ingress.kubernetes.io/service.sticky.cookie: "true" + traefik.ingress.kubernetes.io/service.sticky.cookie.name: "XPEDITIS_BACKEND" + traefik.ingress.kubernetes.io/service.sticky.cookie.secure: "true" + traefik.ingress.kubernetes.io/service.sticky.cookie.httponly: "true" + + # Timeout pour les longues requêtes (carrier APIs = jusqu'à 30s) + traefik.ingress.kubernetes.io/router.middlewares: "xpeditis-prod-ratelimit@kubernetescrd" + + # Headers de sécurité + traefik.ingress.kubernetes.io/router.middlewares: "xpeditis-prod-headers@kubernetescrd" + +spec: + ingressClassName: traefik + tls: + - hosts: + - api.xpeditis.com + - app.xpeditis.com + secretName: xpeditis-tls-prod + + rules: + # API Backend NestJS + - host: api.xpeditis.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: xpeditis-backend + port: + number: 4000 + + # Frontend Next.js + - host: app.xpeditis.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: xpeditis-frontend + port: + number: 3000 +--- +# Middleware : headers de sécurité +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: headers + namespace: xpeditis-prod +spec: + headers: + customRequestHeaders: + X-Forwarded-Proto: "https" + customResponseHeaders: + X-Frame-Options: "SAMEORIGIN" + X-Content-Type-Options: "nosniff" + X-XSS-Protection: "1; mode=block" + Referrer-Policy: "strict-origin-when-cross-origin" + Permissions-Policy: "geolocation=(), microphone=(), camera=()" + contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" + stsSeconds: 31536000 + stsIncludeSubdomains: true + stsPreload: true +--- +# Middleware : rate limiting Traefik (en plus du rate limiting NestJS) +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: ratelimit + namespace: xpeditis-prod +spec: + rateLimit: + average: 100 + burst: 50 + period: 1m + sourceCriterion: + ipStrategy: + depth: 1 +``` + +--- + +## 08 — Horizontal Pod Autoscaler + +```yaml +# k8s/08-hpa.yaml +--- +# HPA Backend +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: backend-hpa + namespace: xpeditis-prod +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: xpeditis-backend + minReplicas: 2 + maxReplicas: 15 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 + behavior: + scaleUp: + stabilizationWindowSeconds: 60 + policies: + - type: Pods + value: 2 + periodSeconds: 60 + scaleDown: + stabilizationWindowSeconds: 300 # 5 min avant de réduire + policies: + - type: Pods + value: 1 + periodSeconds: 120 +--- +# HPA Frontend +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: frontend-hpa + namespace: xpeditis-prod +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: xpeditis-frontend + minReplicas: 1 + maxReplicas: 8 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + behavior: + scaleDown: + stabilizationWindowSeconds: 300 +``` + +--- + +## 09 — PodDisruptionBudget + +```yaml +# k8s/09-pdb.yaml +--- +# Garantit qu'au moins 1 pod backend est toujours disponible pendant les maintenances +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: backend-pdb + namespace: xpeditis-prod +spec: + minAvailable: 1 + selector: + matchLabels: + app: xpeditis-backend +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: frontend-pdb + namespace: xpeditis-prod +spec: + minAvailable: 1 + selector: + matchLabels: + app: xpeditis-frontend +``` + +--- + +## Secret GHCR (GitHub Container Registry) + +Pour que Kubernetes puisse pull les images depuis GHCR : + +```bash +# Créer un Personal Access Token GitHub avec scope: read:packages +# https://github.com/settings/tokens/new + +kubectl create secret docker-registry ghcr-credentials \ + --namespace xpeditis-prod \ + --docker-server=ghcr.io \ + --docker-username= \ + --docker-password= \ + --docker-email= +``` + +--- + +## Déploiement complet + +```bash +# Appliquer tous les manifests dans l'ordre +kubectl apply -f k8s/00-namespaces.yaml +kubectl apply -f k8s/01-secrets.yaml # Après avoir rempli les valeurs +kubectl apply -f k8s/02-configmaps.yaml +kubectl apply -f k8s/03-backend-deployment.yaml +kubectl apply -f k8s/04-backend-service.yaml +kubectl apply -f k8s/05-frontend-deployment.yaml +kubectl apply -f k8s/06-frontend-service.yaml +kubectl apply -f k8s/07-ingress.yaml +kubectl apply -f k8s/08-hpa.yaml +kubectl apply -f k8s/09-pdb.yaml + +# Ou tout d'un coup +kubectl apply -f k8s/ + +# Suivre le déploiement +kubectl rollout status deployment/xpeditis-backend -n xpeditis-prod +kubectl rollout status deployment/xpeditis-frontend -n xpeditis-prod + +# Voir les pods +kubectl get pods -n xpeditis-prod -w + +# Voir les logs +kubectl logs -f deployment/xpeditis-backend -n xpeditis-prod +kubectl logs -f deployment/xpeditis-frontend -n xpeditis-prod + +# Vérifier le certificat TLS +kubectl get certificate -n xpeditis-prod +# NAME READY SECRET AGE +# xpeditis-tls-prod True xpeditis-tls-prod 2m +``` + +--- + +## Migration des jobs TypeORM + +Le déploiement inclut automatiquement les migrations via le `startup.js` dans le Dockerfile. Si vous avez besoin de lancer les migrations manuellement : + +```bash +# Job de migration one-shot +cat > /tmp/migration-job.yaml << 'EOF' +apiVersion: batch/v1 +kind: Job +metadata: + name: xpeditis-migrations + namespace: xpeditis-prod +spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: migrations + image: ghcr.io//xpeditis-backend:latest + command: ["node", "dist/migration-runner.js"] + envFrom: + - secretRef: + name: backend-secrets + imagePullSecrets: + - name: ghcr-credentials +EOF + +kubectl apply -f /tmp/migration-job.yaml +kubectl wait --for=condition=complete job/xpeditis-migrations -n xpeditis-prod --timeout=300s +kubectl logs job/xpeditis-migrations -n xpeditis-prod +kubectl delete job xpeditis-migrations -n xpeditis-prod +``` diff --git a/docs/deployment/hetzner/10-ingress-tls-cloudflare.md b/docs/deployment/hetzner/10-ingress-tls-cloudflare.md new file mode 100644 index 0000000..5184cf9 --- /dev/null +++ b/docs/deployment/hetzner/10-ingress-tls-cloudflare.md @@ -0,0 +1,240 @@ +# 10 — Ingress, TLS et Cloudflare + +--- + +## Architecture TLS + +Deux approches possibles, que vous pouvez combiner : + +``` +Option 1 — TLS Cloudflare uniquement (plus simple) + Browser → Cloudflare (TLS terminé) → HTTP vers Hetzner LB → Pods + +Option 2 — TLS de bout en bout (plus sécurisé) + Browser → Cloudflare → HTTPS vers Hetzner LB → cert-manager TLS → Pods + +Recommandation : Option 2 avec Cloudflare en "Full (strict)" mode +``` + +--- + +## Configuration Cloudflare + +### 1. Ajouter les entrées DNS + +Dans votre dashboard Cloudflare → Votre domaine → DNS → Records : + +``` +Type Name Content Proxy TTL +A api ✅ ON Auto +A app ✅ ON Auto +A @ ✅ ON Auto +``` + +Pour obtenir l'IP du Load Balancer Hetzner : +```bash +hcloud load-balancer list +# ID NAME TYPE LOCATION PUBLIC NET PRIVATE NET +# 12345 xpeditis-lb lb11 fsn1 1.2.3.4 / 2001::... 10.0.0.2 +``` + +### 2. SSL/TLS Mode + +Cloudflare → Votre domaine → SSL/TLS → Overview : +- Sélectionnez **Full (strict)** ← obligatoire si cert-manager gère les certicats côté Hetzner + +### 3. Page Rules / Transform Rules + +Cloudflare → Votre domaine → Rules → Page Rules : + +``` +Rule 1 : Force HTTPS + If URL matches: http://api.xpeditis.com/* + Then: Always Use HTTPS + +Rule 2 : Force HTTPS frontend + If URL matches: http://app.xpeditis.com/* + Then: Always Use HTTPS +``` + +### 4. WAF Rules (optionnel mais recommandé) + +Cloudflare → Security → WAF → Managed Rules : +- Activer **Cloudflare Managed Ruleset** (gratuit) +- Activer **Cloudflare OWASP Core Ruleset** (gratuit) + +Custom Rules pour Xpeditis : +``` +Rule: Block rate search abuse + If: (http.request.uri.path contains "/api/v1/rates/search") AND (rate(1m) > 60) + Then: Block + +Rule: Protect Stripe webhook + If: (http.request.uri.path eq "/api/v1/subscriptions/webhook") AND (not ip.src in {151.101.0.0/17}) + Then: Block ← Autorise uniquement les IPs Stripe +``` + +### 5. Cache Rules (pour les assets frontend) + +Cloudflare → Caching → Cache Rules : +``` +Rule: Cache Next.js static assets + If: (http.request.uri.path contains "/_next/static/") + Then: Cache Everything, TTL 1 year +``` + +--- + +## Vérification du certificat TLS (cert-manager) + +Après le déploiement de l'Ingress : + +```bash +# Vérifier l'état du certificat +kubectl get certificate -n xpeditis-prod +# NAME READY SECRET AGE +# xpeditis-tls-prod True xpeditis-tls-prod 5m ← READY=True = succès + +# Si READY=False, debugger : +kubectl describe certificate xpeditis-tls-prod -n xpeditis-prod +kubectl describe certificaterequest -n xpeditis-prod +kubectl logs -n cert-manager deployment/cert-manager | tail -50 + +# Voir les challenges ACME en cours +kubectl get challenge -n xpeditis-prod +# Si des challenges sont en attente, vérifier que le DNS Cloudflare pointe bien vers le LB +``` + +### Tester la chaîne TLS + +```bash +# Tester le certificat +curl -I https://api.xpeditis.com/api/v1/health +# HTTP/2 200 +# server: traefik +# content-type: application/json + +# Détails du certificat +openssl s_client -connect api.xpeditis.com:443 -servername api.xpeditis.com 2>/dev/null | openssl x509 -noout -dates +# notBefore=Apr 1 00:00:00 2026 GMT +# notAfter=Jun 30 00:00:00 2026 GMT ← Let's Encrypt = 90 jours, renouvellement auto à 60 jours +``` + +--- + +## Configuration WebSocket Socket.IO + +Socket.IO nécessite une configuration spécifique pour fonctionner derrière Traefik + Cloudflare. + +### Cloudflare WebSocket + +Cloudflare → Votre domaine → Network → WebSockets : +- **Activer WebSockets** (désactivé par défaut sur le plan Free) + +> Note : Sur le plan Free Cloudflare, les WebSockets sont supportés mais avec un timeout de 100s. Pour les connexions persistantes Socket.IO, configurez des reconnexions côté client. + +### Traefik Sticky Sessions + +La configuration des sticky sessions dans `k8s/07-ingress.yaml` garantit que les reconnexions WebSocket retombent sur le même pod (important pour Socket.IO avant l'implémentation Redis adapter) : + +```yaml +annotations: + traefik.ingress.kubernetes.io/service.sticky.cookie: "true" + traefik.ingress.kubernetes.io/service.sticky.cookie.name: "XPEDITIS_BACKEND" + traefik.ingress.kubernetes.io/service.sticky.cookie.secure: "true" +``` + +### Test WebSocket + +```bash +# Test avec wscat (npm install -g wscat) +wscat -c "wss://api.xpeditis.com/notifications" \ + -H "Authorization: Bearer " + +# La connexion doit s'établir et recevoir : +# {"event":"unread_count","data":{"count":0}} +# {"event":"recent_notifications","data":[...]} +``` + +--- + +## Traefik Dashboard (accès restreint) + +```bash +# Traefik a un dashboard utile pour debugger les routes +# Activer l'accès avec authentification + +# Générer un mot de passe htpasswd +htpasswd -nb admin | base64 + +# Créer un Middleware BasicAuth +cat > /tmp/traefik-auth.yaml << 'EOF' +apiVersion: v1 +kind: Secret +metadata: + name: traefik-dashboard-auth + namespace: kube-system +type: kubernetes.io/basic-auth +stringData: + username: admin + password: +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: dashboard-auth + namespace: kube-system +spec: + basicAuth: + secret: traefik-dashboard-auth +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: traefik-dashboard + namespace: kube-system + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + traefik.ingress.kubernetes.io/router.middlewares: "kube-system-dashboard-auth@kubernetescrd" +spec: + ingressClassName: traefik + tls: + - hosts: + - traefik.xpeditis.com + secretName: traefik-tls + rules: + - host: traefik.xpeditis.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: traefik + port: + number: 9000 +EOF + +kubectl apply -f /tmp/traefik-auth.yaml +``` + +--- + +## Checklist TLS + +```bash +echo "=== Test endpoints ===" +curl -sf https://api.xpeditis.com/api/v1/health | jq . +curl -sf https://app.xpeditis.com/ | head -5 + +echo "=== Certificats ===" +kubectl get certificate -n xpeditis-prod +kubectl get certificaterequest -n xpeditis-prod + +echo "=== Ingress ===" +kubectl get ingress -n xpeditis-prod + +echo "=== Test HTTPS force ===" +curl -L http://api.xpeditis.com/api/v1/health +# Doit être redirigé vers HTTPS +``` diff --git a/docs/deployment/hetzner/11-cicd-github-actions.md b/docs/deployment/hetzner/11-cicd-github-actions.md new file mode 100644 index 0000000..370d486 --- /dev/null +++ b/docs/deployment/hetzner/11-cicd-github-actions.md @@ -0,0 +1,489 @@ +# 11 — CI/CD avec GitHub Actions + +Pipeline complet : commit → build Docker → push GHCR → déploiement k3s → vérification. + +--- + +## Architecture du pipeline + +``` +Push sur main + │ + ├── Job: test + │ ├── npm run backend:lint + │ ├── npm run backend:test + │ └── npm run frontend:lint + │ + ├── Job: build (si tests OK) + │ ├── docker buildx build backend → ghcr.io//xpeditis-backend:sha + :latest + │ └── docker buildx build frontend → ghcr.io//xpeditis-frontend:sha + :latest + │ + └── Job: deploy (si build OK) + ├── kubectl set image deployment/xpeditis-backend ... + ├── kubectl set image deployment/xpeditis-frontend ... + ├── kubectl rollout status ... + └── Health check final +``` + +--- + +## Secrets GitHub à configurer + +Dans votre repo GitHub → Settings → Secrets and variables → Actions → New repository secret : + +| Secret | Valeur | Usage | +|---|---|---| +| `HETZNER_KUBECONFIG` | Contenu de `~/.kube/kubeconfig-xpeditis-prod` (base64) | Accès kubectl | +| `GHCR_TOKEN` | Personal Access Token GitHub (scope: `write:packages`) | Push images | +| `SLACK_WEBHOOK_URL` | URL webhook Slack (optionnel) | Notifications | + +```bash +# Encoder le kubeconfig en base64 pour GitHub Secrets +cat ~/.kube/kubeconfig-xpeditis-prod | base64 -w 0 +# Copier le résultat dans HETZNER_KUBECONFIG + +# Créer le Personal Access Token GitHub +# https://github.com/settings/tokens/new +# Scopes : write:packages, read:packages, delete:packages +``` + +--- + +## Workflow principal — `.github/workflows/deploy.yml` + +```yaml +# .github/workflows/deploy.yml +name: Build & Deploy to Hetzner + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_BACKEND: ghcr.io/${{ github.repository_owner }}/xpeditis-backend + IMAGE_FRONTEND: ghcr.io/${{ github.repository_owner }}/xpeditis-frontend + +jobs: + # ============================================================ + # JOB 1 : Tests & Lint + # ============================================================ + test: + name: Tests & Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: npm run install:all + + - name: Lint backend + run: npm run backend:lint + + - name: Lint frontend + run: npm run frontend:lint + + - name: Test backend (unit) + run: npm run backend:test -- --passWithNoTests + + - name: TypeScript check frontend + run: | + cd apps/frontend + npm run type-check + + # ============================================================ + # JOB 2 : Build & Push Docker Images + # ============================================================ + build: + name: Build Docker Images + runs-on: ubuntu-latest + needs: test + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + permissions: + contents: read + packages: write + + outputs: + backend_tag: ${{ steps.meta-backend.outputs.version }} + frontend_tag: ${{ steps.meta-frontend.outputs.version }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # ── Backend ── + - name: Extract metadata (backend) + id: meta-backend + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_BACKEND }} + tags: | + type=sha,prefix=sha-,format=short + type=raw,value=latest,enable={{is_default_branch}} + type=raw,value={{date 'YYYY-MM-DD'}},enable={{is_default_branch}} + + - name: Build & Push backend + uses: docker/build-push-action@v5 + with: + context: . + file: apps/backend/Dockerfile + push: true + tags: ${{ steps.meta-backend.outputs.tags }} + labels: ${{ steps.meta-backend.outputs.labels }} + cache-from: type=gha,scope=backend + cache-to: type=gha,mode=max,scope=backend + platforms: linux/amd64 # Changer en linux/amd64,linux/arm64 si vous utilisez des CAX + + # ── Frontend ── + - name: Extract metadata (frontend) + id: meta-frontend + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_FRONTEND }} + tags: | + type=sha,prefix=sha-,format=short + type=raw,value=latest,enable={{is_default_branch}} + type=raw,value={{date 'YYYY-MM-DD'}},enable={{is_default_branch}} + + - name: Build & Push frontend + uses: docker/build-push-action@v5 + with: + context: . + file: apps/frontend/Dockerfile + push: true + tags: ${{ steps.meta-frontend.outputs.tags }} + labels: ${{ steps.meta-frontend.outputs.labels }} + cache-from: type=gha,scope=frontend + cache-to: type=gha,mode=max,scope=frontend + platforms: linux/amd64 + build-args: | + NEXT_PUBLIC_API_URL=https://api.xpeditis.com + NEXT_PUBLIC_APP_URL=https://app.xpeditis.com + NEXT_PUBLIC_API_PREFIX=api/v1 + + # ============================================================ + # JOB 3 : Deploy vers k3s Hetzner + # ============================================================ + deploy: + name: Deploy to Hetzner k3s + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + environment: + name: production + url: https://app.xpeditis.com + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure kubectl + run: | + mkdir -p ~/.kube + echo "${{ secrets.HETZNER_KUBECONFIG }}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + kubectl cluster-info + + - name: Deploy Backend + run: | + BACKEND_TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)" + + kubectl set image deployment/xpeditis-backend \ + backend=${{ env.IMAGE_BACKEND }}:${BACKEND_TAG} \ + -n xpeditis-prod + + kubectl rollout status deployment/xpeditis-backend \ + -n xpeditis-prod \ + --timeout=300s + + echo "✅ Backend deployed: ${BACKEND_TAG}" + + - name: Deploy Frontend + run: | + FRONTEND_TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)" + + kubectl set image deployment/xpeditis-frontend \ + frontend=${{ env.IMAGE_FRONTEND }}:${FRONTEND_TAG} \ + -n xpeditis-prod + + kubectl rollout status deployment/xpeditis-frontend \ + -n xpeditis-prod \ + --timeout=300s + + echo "✅ Frontend deployed: ${FRONTEND_TAG}" + + - name: Health Check + run: | + sleep 15 # Laisser le temps au LB de propager + + # Test API backend + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" \ + https://api.xpeditis.com/api/v1/health) + if [ "$STATUS" != "200" ]; then + echo "❌ Backend health check failed (HTTP $STATUS)" + exit 1 + fi + echo "✅ Backend healthy (HTTP $STATUS)" + + # Test frontend + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" \ + https://app.xpeditis.com/) + if [ "$STATUS" != "200" ]; then + echo "❌ Frontend health check failed (HTTP $STATUS)" + exit 1 + fi + echo "✅ Frontend healthy (HTTP $STATUS)" + + - name: Notify Slack (success) + if: success() + run: | + if [ -n "${{ secrets.SLACK_WEBHOOK_URL }}" ]; then + curl -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \ + -H 'Content-type: application/json' \ + --data '{ + "text": "✅ Xpeditis déployé en production", + "attachments": [{ + "color": "good", + "fields": [ + {"title": "Commit", "value": "${{ github.sha }}", "short": true}, + {"title": "Auteur", "value": "${{ github.actor }}", "short": true}, + {"title": "Message", "value": "${{ github.event.head_commit.message }}", "short": false} + ] + }] + }' + fi + + - name: Notify Slack (failure) + if: failure() + run: | + if [ -n "${{ secrets.SLACK_WEBHOOK_URL }}" ]; then + curl -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \ + -H 'Content-type: application/json' \ + --data '{ + "text": "❌ Échec du déploiement Xpeditis", + "attachments": [{ + "color": "danger", + "fields": [ + {"title": "Commit", "value": "${{ github.sha }}", "short": true}, + {"title": "Job", "value": "${{ github.workflow }}", "short": true} + ] + }] + }' + fi + + - name: Rollback on failure + if: failure() + run: | + echo "⏮️ Rollback en cours..." + kubectl rollout undo deployment/xpeditis-backend -n xpeditis-prod + kubectl rollout undo deployment/xpeditis-frontend -n xpeditis-prod + kubectl rollout status deployment/xpeditis-backend -n xpeditis-prod --timeout=120s + echo "✅ Rollback terminé" +``` + +--- + +## Workflow de staging (PR preview) — `.github/workflows/staging.yml` + +```yaml +# .github/workflows/staging.yml +name: Deploy to Staging + +on: + pull_request: + branches: + - main + +jobs: + build-staging: + name: Build Staging + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build & Push (staging tag) + uses: docker/build-push-action@v5 + with: + context: . + file: apps/backend/Dockerfile + push: true + tags: ghcr.io/${{ github.repository_owner }}/xpeditis-backend:pr-${{ github.event.pull_request.number }} + build-args: NODE_ENV=staging + + - name: Comment PR + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '🐳 Image Docker staging buildée : `pr-${{ github.event.pull_request.number }}`' + }) +``` + +--- + +## Mise à jour des manifests Kubernetes + +Alternativement, vous pouvez mettre à jour les fichiers YAML dans Git et les appliquer : + +```bash +# Dans le workflow CI, mettre à jour le tag d'image dans les manifests +- name: Update image in manifests + run: | + IMAGE_TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)" + + # Mettre à jour les fichiers YAML + sed -i "s|image: ghcr.io/.*/xpeditis-backend:.*|image: ${{ env.IMAGE_BACKEND }}:${IMAGE_TAG}|g" \ + k8s/03-backend-deployment.yaml + + sed -i "s|image: ghcr.io/.*/xpeditis-frontend:.*|image: ${{ env.IMAGE_FRONTEND }}:${IMAGE_TAG}|g" \ + k8s/05-frontend-deployment.yaml + + # Committer les changements (GitOps) + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git add k8s/ + git commit -m "chore: update image tags to ${IMAGE_TAG} [skip ci]" + git push +``` + +--- + +## Dockerfile final — Backend + +```dockerfile +# apps/backend/Dockerfile +FROM node:20-alpine AS deps +RUN apk add --no-cache python3 make g++ +WORKDIR /app +COPY package*.json ./ +COPY apps/backend/package*.json apps/backend/ +RUN npm ci --workspace=apps/backend + +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/apps/backend/node_modules ./apps/backend/node_modules +COPY . . +RUN cd apps/backend && npm run build + +FROM node:20-alpine AS runner +RUN apk add --no-cache dumb-init +RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001 +WORKDIR /app +COPY --from=builder --chown=nestjs:nodejs /app/apps/backend/dist ./dist +COPY --from=builder --chown=nestjs:nodejs /app/apps/backend/node_modules ./node_modules +COPY --from=builder --chown=nestjs:nodejs /app/apps/backend/package.json ./ +USER nestjs +EXPOSE 4000 +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD wget -qO- http://localhost:4000/api/v1/health || exit 1 +ENTRYPOINT ["dumb-init", "--"] +CMD ["node", "dist/main.js"] +``` + +## Dockerfile final — Frontend + +```dockerfile +# apps/frontend/Dockerfile +FROM node:20-alpine AS deps +WORKDIR /app +COPY package*.json ./ +COPY apps/frontend/package*.json apps/frontend/ +RUN npm ci --workspace=apps/frontend + +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/apps/frontend/node_modules ./apps/frontend/node_modules +COPY . . +ARG NEXT_PUBLIC_API_URL +ARG NEXT_PUBLIC_APP_URL +ARG NEXT_PUBLIC_API_PREFIX=api/v1 +ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \ + NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL \ + NEXT_PUBLIC_API_PREFIX=$NEXT_PUBLIC_API_PREFIX \ + NEXT_TELEMETRY_DISABLED=1 +RUN cd apps/frontend && npm run build + +FROM node:20-alpine AS runner +RUN apk add --no-cache dumb-init +RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001 +WORKDIR /app +COPY --from=builder --chown=nextjs:nodejs /app/apps/frontend/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/apps/frontend/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/apps/frontend/public ./public +USER nextjs +EXPOSE 3000 +ENV NODE_ENV=production PORT=3000 HOSTNAME="0.0.0.0" NEXT_TELEMETRY_DISABLED=1 +ENTRYPOINT ["dumb-init", "--"] +CMD ["node", "server.js"] +``` + +--- + +## Test du pipeline en local + +```bash +# Simuler le build Docker localement +docker build \ + -f apps/backend/Dockerfile \ + -t xpeditis-backend:local \ + . + +docker build \ + -f apps/frontend/Dockerfile \ + -t xpeditis-frontend:local \ + --build-arg NEXT_PUBLIC_API_URL=http://localhost:4000 \ + --build-arg NEXT_PUBLIC_APP_URL=http://localhost:3000 \ + . + +# Tester l'image backend +docker run --rm -p 4000:4000 \ + -e NODE_ENV=production \ + -e DATABASE_HOST= \ + -e DATABASE_USER=xpeditis \ + -e DATABASE_PASSWORD= \ + -e DATABASE_NAME=xpeditis \ + -e REDIS_HOST= \ + -e REDIS_PASSWORD= \ + -e JWT_SECRET=test-secret \ + -e SMTP_HOST=localhost \ + -e SMTP_PORT=25 \ + -e SMTP_USER=test \ + -e SMTP_PASS=test \ + xpeditis-backend:local + +curl http://localhost:4000/api/v1/health +``` diff --git a/docs/deployment/hetzner/12-monitoring-alerting.md b/docs/deployment/hetzner/12-monitoring-alerting.md new file mode 100644 index 0000000..42129ba --- /dev/null +++ b/docs/deployment/hetzner/12-monitoring-alerting.md @@ -0,0 +1,416 @@ +# 12 — Monitoring et alertes + +--- + +## Stack de monitoring + +``` +Prometheus ← Scrape des métriques (pods, nodes, app) +Grafana ← Dashboards visuels +Loki ← Agrégation des logs (NestJS pino) +Alertmanager ← Envoi alertes (email, Slack) +Uptime Kuma ← Monitoring externe HTTP (health checks) +``` + +--- + +## Installation du kube-prometheus-stack + +La stack la plus complète, déployée avec Helm : + +```bash +# Ajouter le repo +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +helm repo update + +# Créer le namespace monitoring +kubectl create namespace monitoring + +# Installer kube-prometheus-stack +helm install prometheus prometheus-community/kube-prometheus-stack \ + --namespace monitoring \ + --version 65.3.1 \ + --set grafana.adminPassword="" \ + --set grafana.persistence.enabled=true \ + --set grafana.persistence.size=2Gi \ + --set grafana.persistence.storageClassName=hcloud-volumes \ + --set prometheus.prometheusSpec.retention=7d \ + --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.storageClassName=hcloud-volumes \ + --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=10Gi \ + --set alertmanager.alertmanagerSpec.storage.volumeClaimTemplate.spec.storageClassName=hcloud-volumes \ + --set alertmanager.alertmanagerSpec.storage.volumeClaimTemplate.spec.resources.requests.storage=2Gi \ + --set prometheusOperator.resources.requests.cpu=50m \ + --set prometheusOperator.resources.requests.memory=128Mi \ + --set prometheus.prometheusSpec.resources.requests.cpu=100m \ + --set prometheus.prometheusSpec.resources.requests.memory=512Mi \ + --set grafana.resources.requests.cpu=50m \ + --set grafana.resources.requests.memory=128Mi + +# Attendre que tout soit Running +kubectl rollout status deployment/prometheus-grafana -n monitoring --timeout=300s +kubectl get pods -n monitoring +``` + +--- + +## Exposer Grafana via Ingress + +```bash +cat > /tmp/grafana-ingress.yaml << 'EOF' +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: grafana + namespace: monitoring + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + traefik.ingress.kubernetes.io/router.entrypoints: "websecure" + traefik.ingress.kubernetes.io/router.tls: "true" + # Restreindre aux IPs de l'équipe + traefik.ingress.kubernetes.io/router.middlewares: "monitoring-ipwhitelist@kubernetescrd" +spec: + ingressClassName: traefik + tls: + - hosts: + - monitoring.xpeditis.com + secretName: monitoring-tls + rules: + - host: monitoring.xpeditis.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: prometheus-grafana + port: + number: 80 +--- +# IP Whitelist pour Grafana (votre équipe seulement) +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: ipwhitelist + namespace: monitoring +spec: + ipWhiteList: + sourceRange: + - "/32" + - "10.0.0.0/16" # Réseau interne Hetzner +EOF + +kubectl apply -f /tmp/grafana-ingress.yaml +``` + +--- + +## Installation de Loki (agrégation des logs) + +```bash +helm repo add grafana https://grafana.github.io/helm-charts +helm repo update + +helm install loki grafana/loki-stack \ + --namespace monitoring \ + --set loki.persistence.enabled=true \ + --set loki.persistence.size=5Gi \ + --set loki.persistence.storageClassName=hcloud-volumes \ + --set promtail.enabled=true \ + --set loki.config.limits_config.retention_period=7d \ + --set grafana.enabled=false # On utilise le Grafana déjà installé + +# Ajouter Loki comme datasource dans Grafana +# Grafana → Data Sources → Add → Loki +# URL: http://loki:3100 +``` + +--- + +## Configuration des alertes + +### Alertes Xpeditis spécifiques + +```bash +cat > /tmp/xpeditis-alerts.yaml << 'EOF' +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: xpeditis-alerts + namespace: xpeditis-prod + labels: + release: prometheus +spec: + groups: + - name: xpeditis.backend + interval: 30s + rules: + + # Backend down + - alert: XpeditisBackendDown + expr: up{job="xpeditis-backend"} == 0 + for: 1m + labels: + severity: critical + annotations: + summary: "Backend Xpeditis indisponible" + description: "Aucun pod backend ne répond depuis 1 minute." + + # Trop peu de replicas + - alert: XpeditisBackendLowReplicas + expr: kube_deployment_status_replicas_available{deployment="xpeditis-backend",namespace="xpeditis-prod"} < 1 + for: 2m + labels: + severity: critical + annotations: + summary: "Moins d'1 replica backend disponible" + + # CPU élevé (déclenchement autoscaling probable) + - alert: XpeditisHighCPU + expr: | + sum(rate(container_cpu_usage_seconds_total{ + namespace="xpeditis-prod", + container="backend" + }[5m])) by (pod) > 0.8 + for: 5m + labels: + severity: warning + annotations: + summary: "CPU élevé sur pod {{ $labels.pod }}" + description: "Utilisation CPU > 80% depuis 5 minutes." + + # Mémoire élevée + - alert: XpeditisHighMemory + expr: | + container_memory_usage_bytes{ + namespace="xpeditis-prod", + container="backend" + } / container_spec_memory_limit_bytes{ + namespace="xpeditis-prod", + container="backend" + } > 0.85 + for: 5m + labels: + severity: warning + annotations: + summary: "Mémoire élevée sur pod {{ $labels.pod }}" + + # Taux d'erreur HTTP élevé + - alert: XpeditisHighErrorRate + expr: | + sum(rate(traefik_service_requests_total{ + service=~"xpeditis-prod-xpeditis-backend.*", + code=~"5.." + }[5m])) / + sum(rate(traefik_service_requests_total{ + service=~"xpeditis-prod-xpeditis-backend.*" + }[5m])) > 0.05 + for: 2m + labels: + severity: warning + annotations: + summary: "Taux d'erreur 5xx > 5% sur l'API backend" + + # Pods en CrashLoopBackOff + - alert: XpeditisPodCrashLooping + expr: | + increase(kube_pod_container_status_restarts_total{ + namespace="xpeditis-prod" + }[1h]) > 5 + labels: + severity: critical + annotations: + summary: "Pod {{ $labels.pod }} redémarre trop souvent" + + - name: xpeditis.database + rules: + # Pas d'alerte directe sur Neon (managed) — uniquement si self-hosted + + - name: xpeditis.redis + rules: + # Redis mémoire élevée + - alert: RedisHighMemory + expr: | + redis_memory_used_bytes / + redis_memory_max_bytes > 0.85 + for: 5m + labels: + severity: warning + annotations: + summary: "Redis utilise > 85% de sa mémoire" +EOF + +kubectl apply -f /tmp/xpeditis-alerts.yaml +``` + +### Configuration Alertmanager (Slack) + +```bash +cat > /tmp/alertmanager-config.yaml << 'EOF' +apiVersion: v1 +kind: Secret +metadata: + name: alertmanager-prometheus-kube-prometheus-alertmanager + namespace: monitoring +stringData: + alertmanager.yaml: | + global: + resolve_timeout: 5m + slack_api_url: '' + + route: + group_by: ['alertname', 'namespace'] + group_wait: 10s + group_interval: 10m + repeat_interval: 12h + receiver: 'slack-notifications' + routes: + - match: + severity: critical + receiver: 'slack-critical' + - match: + severity: warning + receiver: 'slack-notifications' + + receivers: + - name: 'slack-notifications' + slack_configs: + - channel: '#xpeditis-monitoring' + icon_url: https://avatars.githubusercontent.com/u/3380462 + title: '{{ template "slack.default.title" . }}' + text: '{{ template "slack.default.text" . }}' + send_resolved: true + + - name: 'slack-critical' + slack_configs: + - channel: '#xpeditis-alerts-critiques' + color: 'danger' + title: '🚨 ALERTE CRITIQUE : {{ .CommonAnnotations.summary }}' + text: '{{ .CommonAnnotations.description }}' + send_resolved: true +EOF + +kubectl apply -f /tmp/alertmanager-config.yaml +``` + +--- + +## Dashboards Grafana recommandés + +Importez ces dashboards depuis grafana.com (ID à entrer dans Grafana → Import) : + +| Dashboard | ID | Usage | +|---|---|---| +| Kubernetes Cluster Overview | 6417 | Vue d'ensemble cluster | +| Kubernetes Deployments | 8588 | Détail des deployments | +| Node Exporter Full | 1860 | Métriques système des nœuds | +| Loki & Promtail | 12611 | Logs agrégés | +| Traefik 2 | 4475 | Métriques ingress/requêtes | + +```bash +# Dans Grafana (https://monitoring.xpeditis.com) +# → + → Import +# → Entrer l'ID et cliquer "Load" +# → Sélectionner la datasource Prometheus +# → Import +``` + +--- + +## Uptime Kuma (monitoring externe) + +Uptime Kuma monitore vos endpoints depuis l'extérieur du cluster, indépendamment de Prometheus : + +```bash +# Déployer Uptime Kuma dans le cluster +cat > /tmp/uptime-kuma.yaml << 'EOF' +apiVersion: apps/v1 +kind: Deployment +metadata: + name: uptime-kuma + namespace: monitoring +spec: + replicas: 1 + selector: + matchLabels: + app: uptime-kuma + template: + metadata: + labels: + app: uptime-kuma + spec: + containers: + - name: uptime-kuma + image: louislam/uptime-kuma:1 + ports: + - containerPort: 3001 + volumeMounts: + - name: data + mountPath: /app/data + resources: + requests: + cpu: 50m + memory: 128Mi + volumes: + - name: data + persistentVolumeClaim: + claimName: uptime-kuma-pvc +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: uptime-kuma-pvc + namespace: monitoring +spec: + accessModes: [ReadWriteOnce] + storageClassName: hcloud-volumes + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: uptime-kuma + namespace: monitoring +spec: + selector: + app: uptime-kuma + ports: + - port: 3001 + targetPort: 3001 +EOF + +kubectl apply -f /tmp/uptime-kuma.yaml +``` + +Monitors à configurer dans Uptime Kuma : + +| Monitor | URL | Intervalle | +|---|---|---| +| API Health | `https://api.xpeditis.com/api/v1/health` | 1 min | +| Frontend | `https://app.xpeditis.com/` | 1 min | +| API Login | `POST https://api.xpeditis.com/api/v1/auth/login` | 5 min | + +--- + +## Commandes de monitoring rapides + +```bash +# Top des pods par consommation CPU/RAM +kubectl top pods -n xpeditis-prod --sort-by=cpu + +# Événements récents du namespace +kubectl get events -n xpeditis-prod --sort-by='.lastTimestamp' | tail -20 + +# Logs backend en temps réel (tous les pods) +stern xpeditis-backend -n xpeditis-prod + +# Logs d'erreurs uniquement +kubectl logs -l app=xpeditis-backend -n xpeditis-prod --since=1h | grep -i error + +# Status des HPAs +kubectl get hpa -n xpeditis-prod + +# Métriques des nœuds +kubectl top nodes +``` diff --git a/docs/deployment/hetzner/13-backup-disaster-recovery.md b/docs/deployment/hetzner/13-backup-disaster-recovery.md new file mode 100644 index 0000000..9b62355 --- /dev/null +++ b/docs/deployment/hetzner/13-backup-disaster-recovery.md @@ -0,0 +1,389 @@ +# 13 — Backups et reprise après sinistre + +--- + +## Stratégie de backup + +| Composant | Méthode | Fréquence | Rétention | Destination | +|---|---|---|---|---| +| PostgreSQL | `pg_dump` via CronJob | Quotidien 3h00 | 30 jours | Hetzner Object Storage | +| PostgreSQL WAL | Streaming (si self-hosted) | Continue | 7 jours | Object Storage | +| Redis | RDB snapshot + AOF | Chaque 5 min | 24h | Volume local | +| Secrets Kubernetes | Export manuel chiffré | Avant chaque changement | Illimité | Hors-cluster (coffre) | +| Fichiers S3 | Versioning objet | Permanent | Voir lifecycle | Object Storage | +| Configs K8s | GitOps dans le repo | À chaque commit | Git history | GitHub | + +**Objectifs :** +- **RPO (Recovery Point Objective) :** 24h max (vous pouvez perdre au plus 24h de données) +- **RTO (Recovery Time Objective) :** 4h max (vous pouvez reconstruire en moins de 4h) + +--- + +## Backup PostgreSQL — Option A (Neon.tech) + +Si vous utilisez Neon.tech, les backups sont **automatiques** : +- Point-in-time recovery (PITR) sur 7 jours (plan Free) ou 30 jours (plan Pro) +- Pas de CronJob à gérer + +Pour créer un backup manuel : +```bash +# Installer la CLI Neon +npm install -g neonctl +neonctl auth + +# Créer un point de restauration (branch) +neonctl branches create \ + --project-id \ + --name "backup-$(date +%Y%m%d)" \ + --parent main +``` + +--- + +## Backup PostgreSQL — Option B (self-hosted) + +### CronJob Kubernetes de backup + +```yaml +# k8s/backup-postgres-cronjob.yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: backup-credentials + namespace: xpeditis-prod +type: Opaque +stringData: + # Même credentials que le backend pour Object Storage + AWS_ACCESS_KEY_ID: "" + AWS_SECRET_ACCESS_KEY: "" + AWS_S3_ENDPOINT: "https://fsn1.your-objectstorage.com" + AWS_S3_BUCKET: "xpeditis-prod" + # Credentials PostgreSQL + PGPASSWORD: "" +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: postgres-backup + namespace: xpeditis-prod +spec: + schedule: "0 3 * * *" # 3h00 chaque nuit + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 5 + jobTemplate: + spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: backup + image: postgres:15-alpine + command: + - /bin/sh + - -c + - | + set -e + echo "=== Démarrage backup PostgreSQL $(date) ===" + + # Variables + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + BACKUP_FILE="/tmp/xpeditis_${TIMESTAMP}.sql.gz" + S3_KEY="backups/postgres/$(date +%Y/%m)/xpeditis_${TIMESTAMP}.sql.gz" + + # Dump PostgreSQL compressé + pg_dump \ + -h ${PGHOST} \ + -p ${PGPORT:-5432} \ + -U ${PGUSER} \ + -d ${PGDATABASE} \ + --no-password \ + --clean \ + --if-exists \ + --format=custom \ + | gzip > ${BACKUP_FILE} + + BACKUP_SIZE=$(du -sh ${BACKUP_FILE} | cut -f1) + echo "Dump créé: ${BACKUP_FILE} (${BACKUP_SIZE})" + + # Upload vers Hetzner Object Storage + apk add --no-cache aws-cli 2>/dev/null || pip install awscli 2>/dev/null + + AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ + AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ + aws s3 cp ${BACKUP_FILE} s3://${AWS_S3_BUCKET}/${S3_KEY} \ + --endpoint-url ${AWS_S3_ENDPOINT} + + echo "✅ Backup uploadé: s3://${AWS_S3_BUCKET}/${S3_KEY}" + + # Nettoyage local + rm ${BACKUP_FILE} + + # Vérifier les anciens backups (garder 30 jours) + echo "Backups existants:" + AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ + AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ + aws s3 ls s3://${AWS_S3_BUCKET}/backups/postgres/ \ + --endpoint-url ${AWS_S3_ENDPOINT} \ + --recursive | tail -10 + + echo "=== Backup terminé $(date) ===" + env: + - name: PGHOST + value: "10.0.1.100" # IP privée serveur PostgreSQL + - name: PGPORT + value: "5432" + - name: PGUSER + value: "xpeditis" + - name: PGDATABASE + value: "xpeditis_prod" + envFrom: + - secretRef: + name: backup-credentials + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi +``` + +```bash +# Appliquer +kubectl apply -f k8s/backup-postgres-cronjob.yaml + +# Tester manuellement (créer un Job depuis le CronJob) +kubectl create job --from=cronjob/postgres-backup test-backup -n xpeditis-prod +kubectl logs -l job-name=test-backup -n xpeditis-prod -f + +# Vérifier que le fichier est arrivé dans S3 +aws s3 ls s3://xpeditis-prod/backups/postgres/ \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com \ + --recursive +``` + +--- + +## Procédure de restauration PostgreSQL + +### Restauration complète (catastrophe totale) + +```bash +# Étape 1 : Lister les backups disponibles +aws s3 ls s3://xpeditis-prod/backups/postgres/ \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com \ + --recursive | sort -r | head -10 + +# Étape 2 : Télécharger le backup le plus récent +aws s3 cp \ + s3://xpeditis-prod/backups/postgres/2026/03/xpeditis_20260323_030001.sql.gz \ + /tmp/restore.sql.gz \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com + +# Étape 3 : Décompresser et restaurer +# ⚠️ Cette commande EFFACE les données existantes +gunzip -c /tmp/restore.sql.gz | pg_restore \ + -h \ + -U xpeditis \ + -d xpeditis_prod \ + --clean \ + --if-exists \ + --no-privileges \ + --no-owner + +# Étape 4 : Vérifier l'intégrité +psql -h -U xpeditis -d xpeditis_prod \ + -c "SELECT COUNT(*) as bookings FROM bookings;" + +psql -h -U xpeditis -d xpeditis_prod \ + -c "SELECT COUNT(*) as users FROM users;" + +# Étape 5 : Redémarrer les pods pour reconnecter +kubectl rollout restart deployment/xpeditis-backend -n xpeditis-prod +``` + +--- + +## Backup des Secrets Kubernetes + +Les secrets ne sont pas dans Git (intentionnel). Sauvegardez-les chiffrés. + +```bash +#!/bin/bash +# scripts/backup-secrets.sh + +set -e +BACKUP_DIR="$HOME/.xpeditis-secrets-backup" +mkdir -p "$BACKUP_DIR" +DATE=$(date +%Y%m%d_%H%M%S) + +# Exporter les secrets (encodés base64) +kubectl get secret backend-secrets -n xpeditis-prod -o yaml > /tmp/backend-secrets-${DATE}.yaml +kubectl get secret frontend-secrets -n xpeditis-prod -o yaml > /tmp/frontend-secrets-${DATE}.yaml +kubectl get secret ghcr-credentials -n xpeditis-prod -o yaml > /tmp/ghcr-creds-${DATE}.yaml + +# Chiffrer avec GPG (ou utiliser un password) +tar czf - /tmp/*-${DATE}.yaml | gpg --symmetric --cipher-algo AES256 \ + > "${BACKUP_DIR}/k8s-secrets-${DATE}.tar.gz.gpg" + +# Nettoyage des fichiers temporaires +rm /tmp/*-${DATE}.yaml + +echo "✅ Secrets sauvegardés dans ${BACKUP_DIR}/k8s-secrets-${DATE}.tar.gz.gpg" + +# Lister les backups existants +ls -la "$BACKUP_DIR"/ +``` + +```bash +# Restaurer les secrets depuis un backup +gpg --decrypt "${BACKUP_DIR}/k8s-secrets-20260323_120000.tar.gz.gpg" | tar xzf - +kubectl apply -f /tmp/backend-secrets-20260323_120000.yaml +``` + +--- + +## Runbook — Reprise après sinistre complète + +Procédure si vous perdez tout le cluster (serveurs détruits) : + +### Étape 1 : Recréer l'infrastructure (30 min) + +```bash +# 1. Recréer le réseau +hcloud network create --name xpeditis-network --ip-range 10.0.0.0/16 +hcloud network add-subnet xpeditis-network --type cloud --network-zone eu-central --ip-range 10.0.1.0/24 + +# 2. Recréer le firewall +# (répéter les commandes du doc 03) + +# 3. Recréer le cluster k3s +hetzner-k3s create --config ~/.xpeditis/cluster.yaml + +# 4. Configurer kubectl +export KUBECONFIG=~/.kube/kubeconfig-xpeditis-prod +``` + +### Étape 2 : Restaurer les secrets (15 min) + +```bash +# Créer le namespace +kubectl apply -f k8s/00-namespaces.yaml + +# Restaurer les secrets depuis le backup chiffré +gpg --decrypt "$HOME/.xpeditis-secrets-backup/k8s-secrets-XXXXXXXX.tar.gz.gpg" | tar xzf - +kubectl apply -f /tmp/backend-secrets-*.yaml +kubectl apply -f /tmp/frontend-secrets-*.yaml +kubectl apply -f /tmp/ghcr-creds-*.yaml + +# Recréer le secret GHCR +kubectl create secret docker-registry ghcr-credentials \ + --namespace xpeditis-prod \ + --docker-server=ghcr.io \ + --docker-username= \ + --docker-password= +``` + +### Étape 3 : Restaurer les services (15 min) + +```bash +# Installer cert-manager +helm install cert-manager jetstack/cert-manager \ + --namespace cert-manager --create-namespace \ + --version v1.15.3 --set installCRDs=true +kubectl apply -f /tmp/cluster-issuers.yaml + +# Déployer l'application +kubectl apply -f k8s/ + +# Attendre +kubectl rollout status deployment/xpeditis-backend -n xpeditis-prod --timeout=300s +``` + +### Étape 4 : Restaurer la base de données (30 min) + +```bash +# Si PostgreSQL self-hosted : +# (Recréer le serveur PostgreSQL si nécessaire, doc 07) +# Puis restaurer depuis le backup S3 + +# Télécharger le backup le plus récent +LATEST=$(aws s3 ls s3://xpeditis-prod/backups/postgres/ \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com \ + --recursive | sort -r | head -1 | awk '{print $4}') + +aws s3 cp s3://xpeditis-prod/$LATEST /tmp/restore.sql.gz \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com + +# Restaurer +gunzip -c /tmp/restore.sql.gz | pg_restore \ + -h -U xpeditis -d xpeditis_prod \ + --clean --if-exists --no-privileges --no-owner +``` + +### Étape 5 : Vérification finale (15 min) + +```bash +# Health checks +curl https://api.xpeditis.com/api/v1/health +curl https://app.xpeditis.com/ + +# Test login +curl -X POST https://api.xpeditis.com/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"admin@test.com","password":"test"}' | jq . + +# Vérifier les données +kubectl exec -it deployment/xpeditis-backend -n xpeditis-prod -- \ + node -e "console.log('Database OK')" + +echo "✅ Système opérationnel. RTO: $(date)" +``` + +--- + +## Test régulier des backups (mensuel) + +```bash +#!/bin/bash +# scripts/test-backup-restore.sh +# À exécuter en environnement de test, JAMAIS en production + +echo "🧪 Test de restauration du backup PostgreSQL" + +# 1. Créer une DB de test +psql -h -U postgres -c "CREATE DATABASE xpeditis_restore_test;" + +# 2. Télécharger le dernier backup +LATEST=$(aws s3 ls s3://xpeditis-prod/backups/postgres/ \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com \ + --recursive | sort -r | head -1 | awk '{print $4}') + +aws s3 cp s3://xpeditis-prod/$LATEST /tmp/test-restore.sql.gz \ + --profile hetzner \ + --endpoint-url https://fsn1.your-objectstorage.com + +# 3. Restaurer dans la DB de test +gunzip -c /tmp/test-restore.sql.gz | pg_restore \ + -h -U postgres -d xpeditis_restore_test + +# 4. Vérifier +BOOKING_COUNT=$(psql -h -U postgres -d xpeditis_restore_test \ + -t -c "SELECT COUNT(*) FROM bookings;" | xargs) + +echo "✅ Restauration réussie. Nombre de bookings: $BOOKING_COUNT" + +# 5. Nettoyage +psql -h -U postgres -c "DROP DATABASE xpeditis_restore_test;" +rm /tmp/test-restore.sql.gz + +echo "✅ Test de backup/restore réussi le $(date)" +``` diff --git a/docs/deployment/hetzner/14-security-hardening.md b/docs/deployment/hetzner/14-security-hardening.md new file mode 100644 index 0000000..12d8fee --- /dev/null +++ b/docs/deployment/hetzner/14-security-hardening.md @@ -0,0 +1,349 @@ +# 14 — Sécurité et hardening + +--- + +## Couches de sécurité + +``` +Internet + │ + ▼ Couche 1 : Cloudflare (WAF, DDoS, Bot protection) + │ + ▼ Couche 2 : Hetzner Firewall (ports, IP whitelist) + │ + ▼ Couche 3 : k3s Network Policies (isolation namespace) + │ + ▼ Couche 4 : NestJS Guards (JWT, Rate Limiting, Roles) + │ + ▼ Couche 5 : PostgreSQL (SSL, auth md5) +``` + +--- + +## Hardening des nœuds Hetzner + +Ces commandes sont exécutées automatiquement via `post_create_commands` dans `cluster.yaml`, mais voici les détails : + +```bash +# Se connecter sur chaque nœud +ssh -i ~/.ssh/xpeditis_hetzner root@ + +# 1. Mettre à jour le système +apt-get update && apt-get upgrade -y + +# 2. Désactiver le login root par mot de passe (SSH key uniquement) +sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config +sed -i 's/PermitRootLogin yes/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config +systemctl restart sshd + +# 3. Configurer fail2ban +apt-get install -y fail2ban +cat > /etc/fail2ban/jail.d/sshd.conf << 'EOF' +[sshd] +enabled = true +maxretry = 3 +bantime = 3600 +findtime = 600 +EOF +systemctl enable fail2ban && systemctl restart fail2ban + +# 4. Configurer le firewall UFW (en plus du firewall Hetzner) +apt-get install -y ufw +ufw default deny incoming +ufw default allow outgoing +ufw allow from 10.0.0.0/16 # Réseau privé Hetzner +ufw allow 22/tcp # SSH +ufw allow 80/tcp # HTTP (LB) +ufw allow 443/tcp # HTTPS (LB) +ufw allow 6443/tcp # K8s API +ufw --force enable + +# 5. Kernel hardening +cat >> /etc/sysctl.d/99-security.conf << 'EOF' +# Désactiver les paquets IP forwardés depuis des sources inconnues +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 + +# Ignorer les ICMP broadcasts +net.ipv4.icmp_echo_ignore_broadcasts = 1 + +# Désactiver l'acceptation des redirections ICMP +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.all.send_redirects = 0 + +# SYN flood protection +net.ipv4.tcp_syncookies = 1 +EOF +sysctl -p /etc/sysctl.d/99-security.conf +``` + +--- + +## Network Policies Kubernetes + +Les NetworkPolicies limitent les communications entre pods : + +```bash +cat > /tmp/network-policies.yaml << 'EOF' +# Politique par défaut : bloquer tout trafic entrant dans le namespace +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-ingress + namespace: xpeditis-prod +spec: + podSelector: {} + policyTypes: + - Ingress + +# Autoriser le trafic depuis Traefik vers le backend +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-traefik-to-backend + namespace: xpeditis-prod +spec: + podSelector: + matchLabels: + app: xpeditis-backend + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + app.kubernetes.io/name: traefik + ports: + - port: 4000 + +# Autoriser le trafic depuis Traefik vers le frontend +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-traefik-to-frontend + namespace: xpeditis-prod +spec: + podSelector: + matchLabels: + app: xpeditis-frontend + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + app.kubernetes.io/name: traefik + ports: + - port: 3000 + +# Autoriser le trafic du backend vers Redis (self-hosted) +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-backend-to-redis + namespace: xpeditis-prod +spec: + podSelector: + matchLabels: + app: redis + ingress: + - from: + - podSelector: + matchLabels: + app: xpeditis-backend + ports: + - port: 6379 + +# Autoriser Prometheus à scraper les métriques du backend +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-prometheus-scrape + namespace: xpeditis-prod +spec: + podSelector: + matchLabels: + app: xpeditis-backend + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: monitoring + ports: + - port: 4000 +EOF + +kubectl apply -f /tmp/network-policies.yaml +``` + +--- + +## Rotation des secrets + +### Script de rotation du JWT secret + +```bash +#!/bin/bash +# scripts/rotate-jwt-secret.sh +# ⚠️ Cette opération déconnecte TOUS les utilisateurs connectés + +set -e +echo "⚠️ Rotation du JWT Secret — tous les utilisateurs seront déconnectés" +read -p "Confirmer ? (yes/no): " CONFIRM +[ "$CONFIRM" != "yes" ] && exit 1 + +# Générer un nouveau secret +NEW_SECRET=$(openssl rand -base64 48) + +# Mettre à jour le Secret Kubernetes +kubectl patch secret backend-secrets -n xpeditis-prod \ + --type='json' \ + -p="[{\"op\":\"replace\",\"path\":\"/data/JWT_SECRET\",\"value\":\"$(echo -n $NEW_SECRET | base64)\"}]" + +# Redémarrer les pods pour prendre en compte le nouveau secret +kubectl rollout restart deployment/xpeditis-backend -n xpeditis-prod + +# Attendre +kubectl rollout status deployment/xpeditis-backend -n xpeditis-prod --timeout=120s + +echo "✅ JWT Secret roté. Tous les utilisateurs devront se reconnecter." +``` + +### Rotation des credentials Hetzner Object Storage + +```bash +# 1. Dans la console Hetzner → Object Storage → Access Keys → Generate new key +# 2. Mettre à jour le Secret Kubernetes avec les nouvelles valeurs +# 3. Redémarrer les pods +kubectl patch secret backend-secrets -n xpeditis-prod \ + --type='json' \ + -p='[ + {"op":"replace","path":"/data/AWS_ACCESS_KEY_ID","value":""}, + {"op":"replace","path":"/data/AWS_SECRET_ACCESS_KEY","value":""} + ]' +kubectl rollout restart deployment/xpeditis-backend -n xpeditis-prod +# 4. Supprimer l'ancienne clé dans la console Hetzner +``` + +--- + +## Sécurisation des accès Kubernetes + +### RBAC — Utilisateur de déploiement limité + +```bash +cat > /tmp/rbac-deploy.yaml << 'EOF' +# Utilisateur de déploiement CI/CD (accès limité au namespace xpeditis-prod) +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ci-deploy + namespace: xpeditis-prod +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: deployer + namespace: xpeditis-prod +rules: +- apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "update", "patch"] +- apiGroups: [""] + resources: ["pods", "pods/log"] + verbs: ["get", "list"] +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: ci-deploy-binding + namespace: xpeditis-prod +subjects: +- kind: ServiceAccount + name: ci-deploy + namespace: xpeditis-prod +roleRef: + kind: Role + name: deployer + apiGroup: rbac.authorization.k8s.io +EOF + +kubectl apply -f /tmp/rbac-deploy.yaml + +# Générer un kubeconfig limité pour le CI (alternative au kubeconfig admin) +SECRET_NAME=$(kubectl get serviceaccount ci-deploy -n xpeditis-prod \ + -o jsonpath='{.secrets[0].name}') +TOKEN=$(kubectl get secret $SECRET_NAME -n xpeditis-prod \ + -o jsonpath='{.data.token}' | base64 -d) + +# Utiliser ce token dans GitHub Secrets pour le CI (plus sécurisé que le kubeconfig admin) +echo "Token CI : $TOKEN" +``` + +--- + +## Audit des accès + +```bash +# Vérifier les dernières connexions SSH sur les nœuds +for NODE in $(hcloud server list -o columns=name --no-header); do + IP=$(hcloud server ip $NODE) + echo "=== $NODE ($IP) ===" + ssh -i ~/.ssh/xpeditis_hetzner root@$IP "last -20 | head -10" +done + +# Vérifier les événements Kubernetes suspects +kubectl get events -A --field-selector type=Warning | grep -v "Normal" + +# Vérifier les tentatives d'accès bloquées par fail2ban +ssh -i ~/.ssh/xpeditis_hetzner root@ \ + "fail2ban-client status sshd" +``` + +--- + +## Checklist de sécurité + +``` +Infrastructure +□ Token API Hetzner limité au projet (read+write minimum nécessaire) +□ Firewall Hetzner : SSH uniquement depuis votre IP +□ fail2ban actif sur tous les nœuds +□ Mises à jour OS automatiques (unattended-upgrades) + +Kubernetes +□ NetworkPolicies appliquées +□ Secrets dans Kubernetes (pas dans les ConfigMaps) +□ k8s/01-secrets.yaml dans .gitignore +□ RBAC CI/CD avec ServiceAccount limité +□ Pod Security Standards activés + +Application +□ JWT_SECRET 48+ caractères aléatoires +□ NEXTAUTH_SECRET différent du JWT_SECRET +□ Stripe en mode live (pas test) en production +□ Sentry configuré pour les erreurs +□ SMTP_FROM vérifié (SPF/DKIM dans Brevo/SendGrid) + +TLS/DNS +□ Cloudflare SSL mode "Full (strict)" +□ HSTS activé (stsPreload: true dans Traefik) +□ Certificats Let's Encrypt valides (READY=True) +□ HTTP → HTTPS redirect actif + +Backups +□ Backup PostgreSQL quotidien testé +□ Secrets Kubernetes sauvegardés chiffrés +□ Test de restauration effectué +``` diff --git a/docs/deployment/hetzner/15-operations-scaling.md b/docs/deployment/hetzner/15-operations-scaling.md new file mode 100644 index 0000000..b7338a0 --- /dev/null +++ b/docs/deployment/hetzner/15-operations-scaling.md @@ -0,0 +1,424 @@ +# 15 — Opérations, Scaling et Troubleshooting + +Référence quotidienne pour gérer le cluster en production. + +--- + +## Commandes kubectl essentielles + +### Vue d'ensemble rapide + +```bash +# État du cluster +kubectl get nodes +kubectl get pods -n xpeditis-prod +kubectl get pods -n xpeditis-prod -o wide # + infos sur les nœuds + +# Ressources consommées +kubectl top nodes +kubectl top pods -n xpeditis-prod --sort-by=cpu + +# Événements récents (erreurs, warnings) +kubectl get events -n xpeditis-prod --sort-by='.lastTimestamp' | tail -30 +kubectl get events -n xpeditis-prod -w # En temps réel + +# État des déploiements +kubectl get deployments -n xpeditis-prod +kubectl get hpa -n xpeditis-prod +kubectl get pvc -n xpeditis-prod +``` + +### Logs + +```bash +# Logs backend (tous les pods) +kubectl logs -l app=xpeditis-backend -n xpeditis-prod --since=1h + +# Logs frontend +kubectl logs -l app=xpeditis-frontend -n xpeditis-prod --since=1h + +# Logs en temps réel (un pod spécifique) +kubectl logs -f pod/xpeditis-backend-5b8d6c7f9-xxxxx -n xpeditis-prod + +# Logs multi-pods en temps réel (avec stern) +stern xpeditis-backend -n xpeditis-prod +stern xpeditis -n xpeditis-prod # Backend + frontend + +# Filtrer les erreurs +kubectl logs -l app=xpeditis-backend -n xpeditis-prod --since=1h | grep -E "ERROR|error|Error" + +# Logs des dernières 100 lignes d'un pod crashé +kubectl logs --previous pod/xpeditis-backend-xxx -n xpeditis-prod | tail -100 +``` + +### Exécution dans un pod + +```bash +# Shell interactif dans un pod backend +kubectl exec -it deployment/xpeditis-backend -n xpeditis-prod -- /bin/sh + +# Commande unique +kubectl exec deployment/xpeditis-backend -n xpeditis-prod -- \ + node -e "console.log(process.env.NODE_ENV)" + +# Vérifier la connectivité DB depuis un pod +kubectl exec deployment/xpeditis-backend -n xpeditis-prod -- \ + nc -zv 10.0.1.100 5432 +``` + +--- + +## Déploiements + +### Déploiement d'une nouvelle version + +```bash +# Via CI/CD (automatique sur push main) — voir doc 11 + +# Manuel : mettre à jour l'image +IMAGE_TAG="sha-$(git rev-parse --short HEAD)" + +kubectl set image deployment/xpeditis-backend \ + backend=ghcr.io//xpeditis-backend:${IMAGE_TAG} \ + -n xpeditis-prod + +# Suivre le déploiement +kubectl rollout status deployment/xpeditis-backend -n xpeditis-prod --timeout=300s + +# Vérifier la version déployée +kubectl get deployment xpeditis-backend -n xpeditis-prod \ + -o jsonpath='{.spec.template.spec.containers[0].image}' +``` + +### Rollback + +```bash +# Rollback vers la version précédente +kubectl rollout undo deployment/xpeditis-backend -n xpeditis-prod + +# Rollback vers une version spécifique +kubectl rollout history deployment/xpeditis-backend -n xpeditis-prod +# REVISION CHANGE-CAUSE +# 1 Initial deployment +# 2 sha-abc1234 +# 3 sha-def5678 ← actuelle + +kubectl rollout undo deployment/xpeditis-backend \ + --to-revision=2 \ + -n xpeditis-prod + +# Vérifier +kubectl rollout status deployment/xpeditis-backend -n xpeditis-prod +``` + +### Redémarrage forcé (sans changer l'image) + +```bash +# Utile après modification des secrets ou configmaps +kubectl rollout restart deployment/xpeditis-backend -n xpeditis-prod +kubectl rollout restart deployment/xpeditis-frontend -n xpeditis-prod + +# Ou redémarrer un pod spécifique (K8s en recrée un nouveau) +kubectl delete pod xpeditis-backend-5b8d6c7f9-xxxxx -n xpeditis-prod +``` + +--- + +## Scaling manuel + +```bash +# Scale horizontal (nombre de pods) +kubectl scale deployment xpeditis-backend \ + --replicas=5 \ + -n xpeditis-prod + +# Scale horizontal frontend +kubectl scale deployment xpeditis-frontend \ + --replicas=3 \ + -n xpeditis-prod + +# Désactiver temporairement le HPA (maintenance) +kubectl patch hpa backend-hpa -n xpeditis-prod \ + -p '{"spec":{"minReplicas":0,"maxReplicas":0}}' + +# Réactiver le HPA +kubectl patch hpa backend-hpa -n xpeditis-prod \ + -p '{"spec":{"minReplicas":2,"maxReplicas":15}}' +``` + +--- + +## Gestion des nœuds + +### Maintenance d'un nœud (drain) + +```bash +# 1. Mettre le nœud en maintenance (draine les pods, bloque les nouveaux) +kubectl cordon xpeditis-prod-cx32-worker-1 +kubectl drain xpeditis-prod-cx32-worker-1 \ + --ignore-daemonsets \ + --delete-emptydir-data \ + --grace-period=30 + +# 2. Effectuer la maintenance (mise à jour OS, etc.) +ssh -i ~/.ssh/xpeditis_hetzner root@ +apt-get update && apt-get upgrade -y +reboot + +# 3. Remettre le nœud en service +kubectl uncordon xpeditis-prod-cx32-worker-1 + +# Vérifier que les pods reviennent +kubectl get pods -n xpeditis-prod -o wide | grep worker-1 +``` + +### Ajouter un nœud worker + +```bash +# Méthode 1 : Via hetzner-k3s (recommandé) +# Modifier cluster.yaml → instance_count: 3 (ou plus) +hetzner-k3s apply --config ~/.xpeditis/cluster.yaml + +# Méthode 2 : Via Cluster Autoscaler (automatique) +# Le CA crée des nœuds quand des pods sont en état "Pending" +# Pour forcer : déployer une charge +kubectl scale deployment xpeditis-backend --replicas=20 -n xpeditis-prod +# Le CA va créer des nœuds automatiquement +# Remettre en place après test +kubectl scale deployment xpeditis-backend --replicas=2 -n xpeditis-prod +``` + +--- + +## Mise à jour de k3s + +Le System Upgrade Controller gère les upgrades automatiquement. Pour une mise à jour manuelle : + +```bash +# Vérifier la version actuelle +kubectl get nodes -o jsonpath='{.items[0].status.nodeInfo.kubeletVersion}' +# v1.30.4+k3s1 + +# Créer un Plan de mise à jour +cat > /tmp/k3s-upgrade.yaml << 'EOF' +apiVersion: upgrade.cattle.io/v1 +kind: Plan +metadata: + name: k3s-server + namespace: system-upgrade +spec: + concurrency: 1 # Un nœud à la fois + cordon: true + serviceAccountName: system-upgrade + version: v1.31.0+k3s1 # Nouvelle version + upgrade: + image: rancher/k3s-upgrade + channel: https://update.k3s.io/v1-release/channels/stable + nodeSelector: + matchExpressions: + - {key: node-role.kubernetes.io/control-plane, operator: Exists} +--- +apiVersion: upgrade.cattle.io/v1 +kind: Plan +metadata: + name: k3s-agent + namespace: system-upgrade +spec: + concurrency: 1 + cordon: true + serviceAccountName: system-upgrade + version: v1.31.0+k3s1 + prepare: + image: rancher/k3s-upgrade + args: ["prepare", "k3s-server"] + upgrade: + image: rancher/k3s-upgrade + channel: https://update.k3s.io/v1-release/channels/stable + nodeSelector: + matchExpressions: + - {key: node-role.kubernetes.io/control-plane, operator: DoesNotExist} +EOF + +kubectl apply -f /tmp/k3s-upgrade.yaml + +# Suivre la progression +kubectl get plans -n system-upgrade +kubectl get jobs -n system-upgrade +``` + +--- + +## Troubleshooting — Problèmes courants + +### Pod en CrashLoopBackOff + +```bash +# 1. Voir les logs du crash +kubectl logs pod/xpeditis-backend-xxx -n xpeditis-prod --previous + +# 2. Décrire le pod +kubectl describe pod xpeditis-backend-xxx -n xpeditis-prod +# Chercher : "Error", "OOMKilled", "Exit Code" + +# Causes fréquentes : +# - OOMKilled (Exit 137) → Augmenter limits.memory +# - Exit 1 → Erreur applicative (DB unreachable, env var manquante) +# - Exit 126 → Problème de permissions sur le fichier d'entrée + +# 3. Si env var manquante +kubectl exec deployment/xpeditis-backend -n xpeditis-prod -- env | sort | grep -E "DB|REDIS|JWT" +``` + +### Pod en Pending (pas démarré) + +```bash +# Voir pourquoi le pod ne démarre pas +kubectl describe pod xpeditis-backend-xxx -n xpeditis-prod | grep -A 20 Events + +# Causes fréquentes : +# "Insufficient cpu/memory" → Pas assez de ressources sur les nœuds → Scale up +# "0/2 nodes are available" → Vérifier les taints/tolerations +# "did not trigger scale-up" → Cluster Autoscaler peut-être désactivé + +# Vérifier le Cluster Autoscaler +kubectl logs -n kube-system deployment/cluster-autoscaler | tail -30 +``` + +### L'API backend retourne des 500 + +```bash +# 1. Vérifier les logs récents +kubectl logs -l app=xpeditis-backend -n xpeditis-prod --since=15m | grep -E "Error|error|500" + +# 2. Tester le health check directement +kubectl exec deployment/xpeditis-backend -n xpeditis-prod -- \ + wget -qO- http://localhost:4000/api/v1/health | jq . + +# 3. Tester la connexion DB +kubectl exec deployment/xpeditis-backend -n xpeditis-prod -- \ + node -e " + const { Client } = require('pg'); + const c = new Client({connectionString: process.env.DATABASE_URL || 'postgres://'+process.env.DATABASE_USER+':'+process.env.DATABASE_PASSWORD+'@'+process.env.DATABASE_HOST+':'+process.env.DATABASE_PORT+'/'+process.env.DATABASE_NAME}); + c.connect().then(() => { console.log('DB OK'); c.end(); }).catch(e => { console.error('DB Error:', e.message); }); + " + +# 4. Tester la connexion Redis +kubectl exec deployment/xpeditis-backend -n xpeditis-prod -- \ + node -e " + const Redis = require('ioredis'); + const r = new Redis({host:process.env.REDIS_HOST,port:process.env.REDIS_PORT,password:process.env.REDIS_PASSWORD}); + r.ping().then(res => { console.log('Redis OK:', res); r.quit(); }).catch(e => { console.error('Redis Error:', e.message); }); + " +``` + +### TLS ne fonctionne pas + +```bash +# Vérifier cert-manager +kubectl get certificates -n xpeditis-prod +kubectl describe certificate xpeditis-tls-prod -n xpeditis-prod + +# Voir les challenges ACME +kubectl get challenges -n xpeditis-prod + +# Si challenge bloqué : vérifier que l'IP du LB est dans le DNS Cloudflare +dig +short api.xpeditis.com +# Doit retourner l'IP du Hetzner LB + +# Forcer le renouvellement du certificat +kubectl delete certificate xpeditis-tls-prod -n xpeditis-prod +kubectl apply -f k8s/07-ingress.yaml # Le certificat sera recréé automatiquement +``` + +### WebSocket se déconnecte fréquemment + +```bash +# 1. Vérifier les sticky sessions dans Traefik +kubectl logs -l app.kubernetes.io/name=traefik -n kube-system | grep -i sticky + +# 2. Vérifier Cloudflare WebSocket est activé +# Cloudflare → Votre domaine → Network → WebSockets → ON + +# 3. Vérifier le timeout WebSocket +# Cloudflare → Rules → Configuration Rules +# Créer une règle pour api.xpeditis.com/* → WebSocket timeout : 300s + +# 4. Vérifier les logs Socket.IO +kubectl logs -l app=xpeditis-backend -n xpeditis-prod | grep -i socket +``` + +--- + +## Mise en maintenance planifiée + +```bash +#!/bin/bash +# scripts/maintenance-mode.sh + +echo "🔧 Activation du mode maintenance" + +# 1. Mettre à jour le ConfigMap pour afficher une page de maintenance +kubectl patch configmap frontend-config -n xpeditis-prod \ + --type='json' \ + -p='[{"op":"add","path":"/data/MAINTENANCE_MODE","value":"true"}]' + +# 2. Redémarrer le frontend +kubectl rollout restart deployment/xpeditis-frontend -n xpeditis-prod +kubectl rollout status deployment/xpeditis-frontend -n xpeditis-prod + +# 3. Prévenir l'équipe +echo "✅ Mode maintenance activé. L'app affiche une page de maintenance." +echo "Pour désactiver : kubectl patch configmap frontend-config -n xpeditis-prod --type='json' -p='[{\"op\":\"remove\",\"path\":\"/data/MAINTENANCE_MODE\"}]'" +``` + +--- + +## Surveillance quotidienne (5 min/jour) + +```bash +#!/bin/bash +# scripts/daily-check.sh +echo "=== Rapport Quotidien Xpeditis $(date) ===" + +echo -e "\n--- CLUSTER ---" +kubectl get nodes +kubectl top nodes + +echo -e "\n--- PODS ---" +kubectl get pods -n xpeditis-prod + +echo -e "\n--- HPA ---" +kubectl get hpa -n xpeditis-prod + +echo -e "\n--- EVENTS RÉCENTS (warnings) ---" +kubectl get events -n xpeditis-prod \ + --field-selector type=Warning \ + --sort-by='.lastTimestamp' | tail -10 + +echo -e "\n--- HEALTH CHECK ---" +curl -sf https://api.xpeditis.com/api/v1/health | jq '.status' || echo "❌ API DOWN" +curl -sf -o /dev/null -w "Frontend: %{http_code}\n" https://app.xpeditis.com/ + +echo -e "\n--- CERTIFICATS ---" +kubectl get certificate -n xpeditis-prod + +echo -e "\n--- STOCKAGE ---" +kubectl get pvc -n xpeditis-prod + +echo "=== Fin du rapport ===" +``` + +--- + +## Liens utiles + +| Ressource | URL | +|---|---| +| Dashboard Grafana | https://monitoring.xpeditis.com | +| API Swagger | https://api.xpeditis.com/api/docs | +| Hetzner Console | https://console.hetzner.cloud | +| Cloudflare Dashboard | https://dash.cloudflare.com | +| Neon Dashboard | https://console.neon.tech | +| Upstash Console | https://console.upstash.com | +| GitHub Actions | https://github.com//xpeditis2.0/actions | +| GitHub GHCR | https://github.com//xpeditis2.0/pkgs/container | diff --git a/docs/deployment/hetzner/README.md b/docs/deployment/hetzner/README.md new file mode 100644 index 0000000..4a17f93 --- /dev/null +++ b/docs/deployment/hetzner/README.md @@ -0,0 +1,111 @@ +# Xpeditis 2.0 — Déploiement Production sur Hetzner Cloud + +> Documentation complète de bout en bout : du choix des serveurs au déploiement production +> Stack : k3s (Kubernetes léger) + Hetzner Object Storage (S3) + PostgreSQL + Redis + +--- + +## Pourquoi ce guide + +Ce guide couvre le déploiement de Xpeditis sur **Hetzner Cloud** avec **k3s** (Kubernetes léger), de la création du compte Hetzner jusqu'à la surveillance en production. C'est l'option la plus économique (€65-450/mois vs $270-5000 sur AWS) tout en restant production-grade. + +**Ce que vous obtiendrez en suivant ce guide :** +- Cluster Kubernetes k3s sur Hetzner avec autoscaling +- Backend NestJS et Frontend Next.js déployés en HA +- PostgreSQL managé (Neon.tech) ou self-hosted selon le budget +- Redis (Upstash) ou self-hosted +- Hetzner Object Storage en remplacement de MinIO (zéro changement de code) +- TLS automatique via Let's Encrypt + Cloudflare +- CI/CD avec GitHub Actions +- Monitoring avec Prometheus + Grafana +- Backups automatisés vers Object Storage +- Runbooks d'opérations et de troubleshooting + +--- + +## Vue d'ensemble des fichiers + +| # | Fichier | Contenu | Temps estimé | +|---|---|---|---| +| — | **README.md** | Ce fichier — index et quickstart | — | +| 01 | [Architecture](./01-architecture.md) | Diagrammes, composants, flux réseau | 15 min lecture | +| 02 | [Prérequis](./02-prerequisites.md) | Outils, comptes, SSH, DNS | 30-60 min setup | +| 03 | [Setup Hetzner](./03-hetzner-setup.md) | Compte, API token, réseau, firewall | 20 min | +| 04 | [Choix des serveurs](./04-server-selection.md) | Sizing par palier, ARM vs x86 | 10 min lecture | +| 05 | [Cluster k3s](./05-k3s-cluster.md) | **Installation complète du cluster** | 30-45 min | +| 06 | [Stockage S3](./06-storage-s3.md) | Hetzner Object Storage, migration MinIO | 15 min | +| 07 | [Base de données](./07-database-postgresql.md) | PostgreSQL (Neon ou self-hosted) | 20-60 min | +| 08 | [Redis](./08-redis-setup.md) | Redis (Upstash ou self-hosted) | 15-30 min | +| 09 | [Manifests Kubernetes](./09-kubernetes-manifests.md) | **Tous les YAMLs complets** | 30 min | +| 10 | [Ingress + TLS](./10-ingress-tls-cloudflare.md) | Traefik, cert-manager, Cloudflare | 30 min | +| 11 | [CI/CD GitHub Actions](./11-cicd-github-actions.md) | Pipeline build + deploy complet | 30 min | +| 12 | [Monitoring](./12-monitoring-alerting.md) | Prometheus, Grafana, Loki, alertes | 45 min | +| 13 | [Backups](./13-backup-disaster-recovery.md) | Stratégie backup + runbook DR | 20 min | +| 14 | [Sécurité](./14-security-hardening.md) | Hardening, network policies, WAF | 30 min | +| 15 | [Opérations](./15-operations-scaling.md) | Scaling, upgrades, troubleshooting | Référence | + +**Temps total de déploiement (première fois) : 4-6 heures** + +--- + +## Quickstart — Du zéro à la production + +Si vous avez déjà tous les prérequis, voici le chemin minimum : + +```bash +# 1. Installer hetzner-k3s +brew install vitobotta/tap/hetzner-k3s + +# 2. Configurer (voir 03-hetzner-setup.md) +export HCLOUD_TOKEN= + +# 3. Créer le cluster (voir 05-k3s-cluster.md) +hetzner-k3s create --config cluster.yaml + +# 4. Configurer kubectl +export KUBECONFIG=~/.kube/kubeconfig-xpeditis-prod + +# 5. Créer les namespaces et secrets +kubectl apply -f k8s/namespaces.yaml +kubectl apply -f k8s/secrets.yaml # après avoir rempli les valeurs + +# 6. Déployer l'application +kubectl apply -f k8s/ + +# 7. Vérifier +kubectl get pods -n xpeditis-prod +``` + +--- + +## Coûts récapitulatifs + +| Palier | Config Hetzner | Coût/mois (post 1er avril 2026) | +|---|---|---| +| **MVP (100 users)** | 1×CX22 + 2×CX32 | **€36** (+ €19 Neon.tech + €0 Upstash free) = **~€55** | +| **Croissance (1 000 users)** | 1×CX22 + 3×CX42 | **€91** (+ DB self-hosted) = **~€110** | +| **Scale (10 000 users)** | 3×CX22 + 6×CX52 | **€340** (+ DB self-hosted HA) = **~€390** | + +--- + +## Architecture résumée + +``` +Internet → Cloudflare (WAF + CDN) → Hetzner LB → k3s Ingress (Traefik) + ├── api.xpeditis.com → NestJS pods + └── app.xpeditis.com → Next.js pods + ↓ + PostgreSQL (Neon / self-hosted) + Redis (Upstash / self-hosted) + Hetzner Object Storage (S3-compatible) +``` + +--- + +## Conventions utilisées dans ce guide + +- `` — à remplacer par votre valeur +- `xpeditis-prod` — namespace Kubernetes de production +- `fsn1` — région Hetzner par défaut (Falkenstein, Allemagne) +- Les commandes `kubectl` supposent `KUBECONFIG` déjà configuré +- Les prix sont en EUR, basés sur les tarifs Hetzner post 1er avril 2026