This commit is contained in:
David 2026-03-26 18:08:28 +01:00
parent 420e52311c
commit 6adcb2b9f8
19 changed files with 6686 additions and 8 deletions

View File

@ -217,6 +217,7 @@ Redis with 15-min TTL for rate quotes. Key format: `rate:{origin}:{destination}:
- RBAC Roles: ADMIN, MANAGER, USER, VIEWER, CARRIER - RBAC Roles: ADMIN, MANAGER, USER, VIEWER, CARRIER
- JWT: access token 15min, refresh token 7d - JWT: access token 15min, refresh token 7d
- Password hashing: Argon2 - Password hashing: Argon2
- OAuth providers: Google, Microsoft (configured via passport strategies)
### Carrier Portal Workflow ### Carrier Portal Workflow
1. Admin creates CSV booking → assigns carrier 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) 1. **Domain Entity**`domain/entities/*.entity.ts` (pure TS, unit tests)
2. **Value Objects**`domain/value-objects/*.vo.ts` (immutable) 2. **Value Objects**`domain/value-objects/*.vo.ts` (immutable)
3. **Port Interface**`domain/ports/out/*.repository.ts` (with token constant) 3. **In Port (Use Case)**`domain/ports/in/*.use-case.ts` (interface with `execute()`)
4. **ORM Entity**`infrastructure/persistence/typeorm/entities/*.orm-entity.ts` 4. **Out Port (Repository)**`domain/ports/out/*.repository.ts` (with token constant)
5. **Migration**`npm run migration:generate -- src/infrastructure/persistence/typeorm/migrations/MigrationName` 5. **ORM Entity**`infrastructure/persistence/typeorm/entities/*.orm-entity.ts`
6. **Repository Impl**`infrastructure/persistence/typeorm/repositories/` 6. **Migration**`npm run migration:generate -- src/infrastructure/persistence/typeorm/migrations/MigrationName`
7. **Mapper**`infrastructure/persistence/typeorm/mappers/` (static toOrm/toDomain/toDomainMany) 7. **Repository Impl**`infrastructure/persistence/typeorm/repositories/`
8. **DTOs**`application/dto/` (with class-validator decorators) 8. **Mapper**`infrastructure/persistence/typeorm/mappers/` (static toOrm/toDomain/toDomainMany)
9. **Controller**`application/controllers/` (with Swagger decorators) 9. **DTOs**`application/dto/` (with class-validator decorators)
10. **Module** → Register and import in `app.module.ts` 10. **Controller**`application/controllers/` (with Swagger decorators)
11. **Module** → Register repository + use-case providers, import in `app.module.ts`
## Documentation ## 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` - Setup guide: `docs/installation/START-HERE.md`
- Carrier Portal API: `apps/backend/docs/CARRIER_PORTAL_API.md` - Carrier Portal API: `apps/backend/docs/CARRIER_PORTAL_API.md`
- Full docs index: `docs/README.md` - Full docs index: `docs/README.md`
- Development roadmap: `TODO.md`
- Infrastructure configs (CI/CD, Docker): `infra/`

View File

@ -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é : 7090% 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 (~50200K lignes/mois à 1 000 users)
- Extensions PostgreSQL : `pg_trgm` (recherche textuelle sur ports)
**S3 (remplace MinIO) :**
- PDFs booking : 100500 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 : 13 secondes par carrier
- Volume estimé : 1030% 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é : 200400 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) | 1020 | 100300 | 1 0003 000 |
| Rate searches / jour | 200500 | 2 0008 000 | 20 00080 000 |
| Bookings créés / mois | 2050 | 300800 | 3 0008 000 |
| Connexions WebSocket simultanées | 1020 | 100300 | 1 0003 000 |
| Emails / mois | 200500 | 5 00020 000 | 50 000200 000 |
| Volume S3 total | 520 GB | 100300 GB | 13 TB |
| Upload S3 / mois | 15 GB | 2050 GB | 200500 GB |
| Trafic sortant APIs carriers / mois | 520 GB | 100300 GB | 13 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 36 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 412 | **$560** |
| Worker nodes — frontend | 2× `m6i.large` (2 vCPU, 8 GB) — autoscaling 26 | **$140** |
| **Total compute** | | **$773** |
> Pods NestJS : 815 replicas (1.5 CPU / 1.5 GB chacun). HPA + KEDA si adoption de SQS.
> Pods Next.js : 48 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) à 3060 min pour les routes peu demandées.
### 🔴 Risque élevé — audit_logs à 10 000 users
À 10 000 users, `audit_logs` peut atteindre **50200M 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 : 5070% 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 5060% 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 6070% 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 ~510% 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.

View File

@ -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** | 215 pods NestJS + 18 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 :** 1020 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 :** 100300 simultanés, ~500 bookings/mois, 5 000 searches/jour, 200 GB stockage, 15 000 emails/mois, PostgreSQL Multi-AZ ou HA
### Configuration retenue
- 34 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 0003 000 simultanés, ~5 000 bookings/mois, 50 000 searches/jour, 1-2 TB stockage, 150 000 emails/mois
### Configuration retenue
- 68 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 410× 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.*

View File

@ -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/<org>/xpeditis-backend:latest` | 215 | 4000 |
| **xpeditis-frontend** | `ghcr.io/<org>/xpeditis-frontend:latest` | 18 | 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=<hetzner_access_key>
AWS_SECRET_ACCESS_KEY=<hetzner_secret_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

View File

@ -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="<votre_token_hetzner>"
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="<votre_org_github>"
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="<upstash_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="<access_key>"
export HETZNER_S3_SECRET_KEY="<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é"
```

View File

@ -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: <votre_access_key>
# AWS Secret Access Key: <votre_secret_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="<votre_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="<access_key>"
export HETZNER_S3_SECRET_KEY="<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"
```

View File

@ -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)

View File

@ -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: "<VOTRE_HCLOUD_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:
- "<VOTRE_IP>/32"
api_allowed_networks:
- "<VOTRE_IP>/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://<IP>:6443
# CoreDNS is running at https://<IP>: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 <none> 4m v1.30.4+k3s1
# xpeditis-prod-cx32-worker-2 Ready <none> 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
```

View File

@ -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=<hetzner_access_key>
AWS_SECRET_ACCESS_KEY=<hetzner_secret_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=<votre_hetzner_access_key>
AWS_SECRET_ACCESS_KEY=<votre_hetzner_secret_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: "<hetzner_access_key>"
AWS_SECRET_ACCESS_KEY: "<hetzner_secret_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**.

View File

@ -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 <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:<password>@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:<password>@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=<neon_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=<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@<IP_PUBLIQUE>
# 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 '<MOT_DE_PASSE_FORT>';
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<hash>"' > /etc/pgbouncer/userlist.txt
# Pour générer le hash md5 :
echo -n "md5$(echo -n '<MOT_DE_PASSE>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=<POSTGRES_PRIVATE_IP>
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=<POSTGRES_PRIVATE_IP> \
DATABASE_PORT=5432 \ # Direct PostgreSQL pour les migrations (pas PgBouncer)
DATABASE_USER=xpeditis \
DATABASE_PASSWORD=<password> \
DATABASE_NAME=xpeditis_prod \
npm run migration:run
# Vérifier
DATABASE_HOST=<POSTGRES_PRIVATE_IP> \
DATABASE_PORT=5432 \
DATABASE_USER=xpeditis \
DATABASE_PASSWORD=<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)
- 5005 000 users → **Self-hosted CCX23** (plus économique à ce niveau)
- > 5 000 users → **Self-hosted CCX33 + replica** (contrôle total)

View File

@ -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=<upstash_token>
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 <password> --tls ping
# PONG
# Test de set/get
redis-cli -h your-redis.upstash.io -p 6379 -a <password> --tls \
SET test:connection "xpeditis-ok" EX 60
redis-cli -h your-redis.upstash.io -p 6379 -a <password> --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<number>('REDIS_PORT', 6379),
password: configService.get('REDIS_PASSWORD'),
db: configService.get<number>('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: "<MOT_DE_PASSE_FORT_REDIS>"
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 <MOT_DE_PASSE_FORT_REDIS>
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 <password> ping
# PONG
```
### Variables d'environnement pour Redis self-hosted
```bash
REDIS_HOST=redis.xpeditis-prod.svc.cluster.local
REDIS_PORT=6379
REDIS_PASSWORD=<MOT_DE_PASSE_FORT_REDIS>
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 <password>
# 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)

View File

@ -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: "<NEON_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: "<PG_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: "<UPSTASH_TOKEN>"
REDIS_DB: "0"
# === Option B : Self-hosted ===
# REDIS_HOST: "redis.xpeditis-prod.svc.cluster.local"
# REDIS_PORT: "6379"
# REDIS_PASSWORD: "<REDIS_PASSWORD>"
# REDIS_DB: "0"
# JWT
JWT_SECRET: "<CHAINE_ALEATOIRE_64_CHARS>"
JWT_ACCESS_EXPIRATION: "15m"
JWT_REFRESH_EXPIRATION: "7d"
# OAuth2 Google
GOOGLE_CLIENT_ID: "<GOOGLE_CLIENT_ID>"
GOOGLE_CLIENT_SECRET: "<GOOGLE_CLIENT_SECRET>"
GOOGLE_CALLBACK_URL: "https://api.xpeditis.com/api/v1/auth/google/callback"
# OAuth2 Microsoft
MICROSOFT_CLIENT_ID: "<MICROSOFT_CLIENT_ID>"
MICROSOFT_CLIENT_SECRET: "<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: "<BREVO_LOGIN>"
SMTP_PASS: "<BREVO_SMTP_KEY>"
SMTP_FROM: "noreply@xpeditis.com"
# Hetzner Object Storage (remplace MinIO)
AWS_S3_ENDPOINT: "https://fsn1.your-objectstorage.com"
AWS_ACCESS_KEY_ID: "<HETZNER_ACCESS_KEY>"
AWS_SECRET_ACCESS_KEY: "<HETZNER_SECRET_KEY>"
AWS_REGION: "eu-central-1"
AWS_S3_BUCKET: "xpeditis-prod"
# Carrier APIs
MAERSK_API_KEY: "<MAERSK_API_KEY>"
MAERSK_API_URL: "https://api.maersk.com/v1"
MSC_API_KEY: "<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_ID>"
CMACGM_CLIENT_SECRET: "<CMACGM_CLIENT_SECRET>"
HAPAG_API_URL: "https://api.hapag-lloyd.com/v1"
HAPAG_API_KEY: "<HAPAG_API_KEY>"
ONE_API_URL: "https://api.one-line.com/v1"
ONE_USERNAME: "<ONE_USERNAME>"
ONE_PASSWORD: "<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: "<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: "<CHAINE_ALEATOIRE_32_CHARS>"
GOOGLE_CLIENT_ID: "<GOOGLE_CLIENT_ID>"
GOOGLE_CLIENT_SECRET: "<GOOGLE_CLIENT_SECRET>"
MICROSOFT_CLIENT_ID: "<MICROSOFT_CLIENT_ID>"
MICROSOFT_CLIENT_SECRET: "<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/<VOTRE_ORG>/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/<VOTRE_ORG>/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=<VOTRE_USERNAME_GITHUB> \
--docker-password=<VOTRE_GITHUB_PAT> \
--docker-email=<VOTRE_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/<VOTRE_ORG>/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
```

View File

@ -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 <IP_LB_HETZNER> ✅ ON Auto
A app <IP_LB_HETZNER> ✅ ON Auto
A @ <IP_LB_HETZNER> ✅ 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 <JWT_TOKEN>"
# 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 <MOT_DE_PASSE> | 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: <MOT_DE_PASSE>
---
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
```

View File

@ -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/<org>/xpeditis-backend:sha + :latest
│ └── docker buildx build frontend → ghcr.io/<org>/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=<votre_db_host> \
-e DATABASE_USER=xpeditis \
-e DATABASE_PASSWORD=<password> \
-e DATABASE_NAME=xpeditis \
-e REDIS_HOST=<votre_redis_host> \
-e REDIS_PASSWORD=<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
```

View File

@ -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="<MOT_DE_PASSE_FORT>" \
--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:
- "<VOTRE_IP>/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: '<SLACK_WEBHOOK_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
```

View File

@ -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 <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: "<HETZNER_ACCESS_KEY>"
AWS_SECRET_ACCESS_KEY: "<HETZNER_SECRET_KEY>"
AWS_S3_ENDPOINT: "https://fsn1.your-objectstorage.com"
AWS_S3_BUCKET: "xpeditis-prod"
# Credentials PostgreSQL
PGPASSWORD: "<PG_PASSWORD>"
---
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 <POSTGRES_HOST> \
-U xpeditis \
-d xpeditis_prod \
--clean \
--if-exists \
--no-privileges \
--no-owner
# Étape 4 : Vérifier l'intégrité
psql -h <POSTGRES_HOST> -U xpeditis -d xpeditis_prod \
-c "SELECT COUNT(*) as bookings FROM bookings;"
psql -h <POSTGRES_HOST> -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=<GITHUB_USERNAME> \
--docker-password=<GITHUB_PAT>
```
### É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 <POSTGRES_HOST> -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 <TEST_HOST> -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 <TEST_HOST> -U postgres -d xpeditis_restore_test
# 4. Vérifier
BOOKING_COUNT=$(psql -h <TEST_HOST> -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 <TEST_HOST> -U postgres -c "DROP DATABASE xpeditis_restore_test;"
rm /tmp/test-restore.sql.gz
echo "✅ Test de backup/restore réussi le $(date)"
```

View File

@ -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@<NODE_IP>
# 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":"<NEW_KEY_BASE64>"},
{"op":"replace","path":"/data/AWS_SECRET_ACCESS_KEY","value":"<NEW_SECRET_BASE64>"}
]'
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@<NODE_IP> \
"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é
```

View File

@ -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/<ORG>/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@<NODE_IP>
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/<ORG>/xpeditis2.0/actions |
| GitHub GHCR | https://github.com/<ORG>/xpeditis2.0/pkgs/container |

View File

@ -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=<votre_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
- `<VALEUR>` — à 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