diff --git a/DOCKER-COMPOSE.md b/DOCKER-COMPOSE.md new file mode 100644 index 000000000..27eb81ca0 --- /dev/null +++ b/DOCKER-COMPOSE.md @@ -0,0 +1,104 @@ +# Docker Compose – GrandNode (school project) + +Run GrandNode e-commerce with MongoDB using Docker Compose. + +## What runs + +| Service | Port | Role | +|----------|-------|--------------------------------| +| `mongodb`| 27017 | MongoDB 7 – database | +| `grandnode` | 8080 | ASP.NET Core app – store UI & API | + +- **Storefront:** http://localhost:8080 +- **Admin:** http://localhost:8080/admin (see README for demo credentials) + +## Quick start + +```bash +# Build and start (required: grandnode image is built locally, not pulled) +docker compose up -d --build + +# View logs +docker compose logs -f grandnode +``` + +If you see "pull access denied for grandnode-school", you tried to pull instead of build. Use `--build` as above. + +### ERR_CONNECTION_REFUSED on localhost:8080 + +1. **Check container status** — GrandNode may be crashing on startup: + ```bash + docker ps -a + ``` + If `grandnode-web` is "Restarting" or "Exited", check logs (step 2). + +2. **Check logs** for errors (e.g. missing config, MongoDB connection): + ```bash + docker compose logs grandnode --tail 100 + ``` + Look for "Now listening on" (success) or exception messages. + +3. **App_Data volume** — If you previously had an `App_Data` volume mounted, it may have hidden `appsettings.json` and caused a crash. The current compose no longer mounts a new App_Data volume so the app can start; restart with: + ```bash + docker compose down && docker compose up -d --build + ``` + +4. **Try explicit URL** — Use http://127.0.0.1:8080 in the browser (compose binds to 127.0.0.1:8080 on the host). + +### "Value cannot be null. (Parameter 'databaseName')" + +The MongoDB connection string must include a **database name**. Use a URL like `mongodb://mongodb:27017/grandnode` (not just `mongodb://mongodb:27017`). The compose file sets this via `ConnectionStrings__Mongodb`. + +After MongoDB is healthy, GrandNode starts and listens on port 8080. + +**After first-time installation:** GrandNode will ask you to restart. Run `docker compose restart grandnode`. The installer is disabled via env var so you won’t see the install screen again. + +## Commands + +```bash +# Stop +docker compose down + +# Stop and remove volumes (reset DB and app data) +docker compose down -v + +# Rebuild app after code changes +docker compose build grandnode && docker compose up -d grandnode +``` + +## Options + +### Build from source (default) + +The main `docker-compose.yml` builds GrandNode from the repo Dockerfile. Use this when you change code or need a custom build. + +### Use official image (no build) + +To run without building (faster start, official release): + +```bash +copy docker-compose.override.example.yml docker-compose.override.yml +docker compose up -d +``` + +Override file swaps the built image for `grandnode/grandnode2:latest`. + +## Data + +- **MongoDB:** `mongodb_data` volume +- **GrandNode images:** `grandnode_images` volume +- **GrandNode App_Data:** Not mounted (using image defaults so the app can start). Settings/plugins in App_Data do not persist across container recreation. + +Data in MongoDB and product images persists across `docker compose down`. Use `docker compose down -v` to wipe volumes. + +## Troubleshooting + +- **GrandNode exits or “cannot connect to MongoDB”** + Wait for MongoDB healthcheck to pass (about 10–15 s on first start), then restart: + `docker compose restart grandnode` + +- **Port 8080 in use** + Change the host port in `docker-compose.yml`, e.g. `"8888:8080"`. + +- **Need different MongoDB URL** + Set `ConnectionStrings__Mongodb` in the `grandnode` service `environment` section. diff --git a/cloudWatch.md b/cloudWatch.md new file mode 100644 index 000000000..5f439df87 --- /dev/null +++ b/cloudWatch.md @@ -0,0 +1,46 @@ +Application Load Balancer (ALB) : * RequestCount : Pour voir l'évolution du trafic. + +TargetResponseTime : Crucial pour l'e-commerce. Si le temps de réponse dépasse 1s, vous perdez des ventes. + +HTTPCode_Target_5XX_Count : Pour détecter immédiatement des erreurs serveurs. + +Auto Scaling Group (ASG) : + +GroupInServiceInstances : Pour vérifier que vos serveurs s'ajoutent bien pendant le pic. + +Base de données (RDS) : on laisse en suspends si je trouve plus rentable car très couteux + +CPUUtilization et DatabaseConnections. + +ReadLatency / WriteLatency. + +Alarme de Scaling : Créez une alarme basée sur le RequestCountPerTarget de votre Load Balancer. Si chaque serveur reçoit trop de requêtes, CloudWatch déclenche l'ajout de nouvelles instances. + +Alarme de Santé : Si le taux d'erreurs 5XX dépasse 1 % sur 2 minutes, recevez une notification immédiate via Amazon SNS (email ou SMS). + +Alarme de Facturation (Billing) : Avec 90 000 visiteurs, les coûts peuvent grimper vite. Mettre une alerte si votre budget dépasse un certain seuil. + + + +CloudWatch Logs : Centralisez les logs de vos serveurs Web (Nginx/Apache) et de votre application. + +CloudWatch Logs Insights : Utilisez cet outil pour faire des requêtes rapides sur des millions de lignes de logs. + +Exemple : "Affiche-moi les 10 pages les plus lentes durant les 15 dernières minutes." + +Contributor Insights : Idéal pour identifier les "Top talkers". Par exemple, si une adresse IP spécifique bombarde votre site (attaque DDoS ou bot), vous le verrez instantanément. + + +Créez un CloudWatch Dashboard unique qui regroupe les indicateurs métier et techniques. Pour 90k visiteurs, votre dashboard devrait afficher : + +Le trafic en temps réel (Nombre de requêtes/sec). + +Le temps de latence moyen (Expérience utilisateur). + +L'état de santé de la base de données. + +Le nombre d'instances EC2 actives. + + + + \ No newline at end of file diff --git a/docker-compose.override.example.yml b/docker-compose.override.example.yml new file mode 100644 index 000000000..c8dab891a --- /dev/null +++ b/docker-compose.override.example.yml @@ -0,0 +1,12 @@ +# Optional: use pre-built GrandNode image instead of building from source. +# Copy to docker-compose.override.yml to skip build and start faster: +# +# copy docker-compose.override.example.yml docker-compose.override.yml +# +# Then: docker compose up -d + +# Setting build to null makes Compose use only the image (no build). +services: + grandnode: + image: grandnode/grandnode2:latest + build: ~ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..5d3366052 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +# Docker Compose for GrandNode e-commerce (school project) +# Runs GrandNode (ASP.NET Core) + MongoDB with persistent volumes. + +services: + mongodb: + image: mongo:7 + container_name: grandnode-mongodb + restart: unless-stopped + ports: + - "27017:27017" + volumes: + - mongodb_data:/data/db + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + grandnode: + build: + context: . + dockerfile: Dockerfile + # Local image name (built, not pulled from a registry) + image: grandnode-school:latest + container_name: grandnode-web + restart: unless-stopped + # Bind to 127.0.0.1 on host to avoid connection issues on Windows + ports: + - "127.0.0.1:8080:8080" + environment: + # MongoDB connection: host + database name (required; null causes startup crash) + ConnectionStrings__Mongodb: "mongodb://mongodb:27017/grandnode" + ASPNETCORE_ENVIRONMENT: Production + ASPNETCORE_URLS: "http://+:8080" + # Disable installer after first setup (overrides appsettings FeatureManagement) + FeatureManagement__Grand__Module__Installer: "false" + depends_on: + mongodb: + condition: service_healthy + volumes: + - grandnode_images:/app/wwwroot/assets/images + # App_Data: do not mount a new volume here — it would hide appsettings.json + # and other files from the image and cause startup failure (ERR_CONNECTION_REFUSED). + # Uncomment below for persistence (requires init in Dockerfile/entrypoint): + # - grandnode_appdata:/app/App_Data + +volumes: + mongodb_data: + grandnode_images: + # grandnode_appdata: diff --git a/load-test/README.md b/load-test/README.md new file mode 100644 index 000000000..3fd433ce7 --- /dev/null +++ b/load-test/README.md @@ -0,0 +1,106 @@ +# Tests de charge avec k6 (GrandNode) + +Tests de charge du storefront GrandNode avec [k6](https://k6.io). Scénarios prévus pour **~90k utilisateurs** (configurable). + +## Prérequis + +- GrandNode en marche (ex. `docker compose up -d` à la racine du projet ; app sur http://127.0.0.1:8080). +- k6 installé en local, **ou** Docker pour lancer k6 en conteneur. + +## Installer k6 (optionnel) + +- **Windows (scoop) :** `scoop install k6` +- **macOS :** `brew install k6` +- **Linux :** voir [Installation k6](https://k6.io/docs/getting-started/installation/). + +Sinon utiliser Docker (sans installation) : voir « Lancer avec Docker » ci-dessous. + +## Lancer rapidement + +Depuis le dossier `load-test/`, ou depuis la racine du projet en adaptant les chemins : + +```bash +# 1k utilisateurs – test en local (montée 2 min → 1000 VUs, palier 3 min, descente 1 min) +k6 run -e SCENARIO=1k k6/storefront.js + +# Par défaut : 50 VUs, 2 min, montée/descente +k6 run k6/storefront.js + +# Charge personnalisée +k6 run -e VUS=200 -e DURATION=5m k6/storefront.js + +# Scénario 90k : montée jusqu’à 3k VUs sur ~1 h (nombre total d’itérations >> 90k) +k6 run -e SCENARIO=90k -e BASE_URL=http://127.0.0.1:8080 k6/storefront.js + +# Progressif 5K → 20K → 50K (durée totale ~45 min) +k6 run -e SCENARIO=progressive k6/storefront.js +# Ou avec le script : .\run-progressive.ps1 +``` + +## Variables d’environnement + +| Variable | Défaut | Description | +|------------|------------------------|-------------| +| `BASE_URL` | http://127.0.0.1:8080 | URL de base de GrandNode. | +| `VUS` | 50 | Nombre cible d’utilisateurs virtuels (scénario par défaut). | +| `DURATION` | 2m | Durée du palier de charge (scénario par défaut). | +| `SCENARIO` | (aucun) | `1k` = 1000 VUs (local), `90k` = montée longue, `progressive` = 5K→20K→50K. | + +## 1k utilisateurs (test en local) + +- **SCENARIO=1k** + Montée en 2 min à 1000 VUs, palier 3 min à 1000 VUs, descente 1 min. Durée totale ~6 min. + Idéal pour tester sur ta machine avant des scénarios plus lourds. + +## Tests progressifs 5K → 20K → 50K + +- **SCENARIO=progressive** + Test de charge progressif avec paliers standard : + - Montée 5 min → 5 000 VUs, palier 5 min à 5K + - Montée 10 min → 20 000 VUs, palier 5 min à 20K + - Montée 10 min → 50 000 VUs, palier 5 min à 50K + - Descente 5 min → 0 + Durée totale ~45 min. Permet d’observer le comportement à chaque palier (débit, latence, erreurs). + +## 90k utilisateurs + +- **SCENARIO=90k** + Montée : 5 min → 500 VUs, 20 min → 2000, 30 min à 3000, 10 min → 1000, 2 min → 0. + Durée totale ~67 minutes ; le nombre total de requêtes HTTP / itérations sera bien supérieur à 90k. + +- **~90k itérations en personnalisé** + Pour viser ~90k itérations au total avec un nombre fixe de VUs, tu peux utiliser un exécuteur constant-vus et une durée telle que (VUs × itérations par VU) ≈ 90k, ou dupliquer le script et fixer un nombre d’itérations dans k6. + +## Lancer avec Docker + +Depuis la **racine du projet** (pour que `BASE_URL` puisse cibler l’hôte si besoin) : + +```powershell +# Le conteneur k6 atteint l’app sur l’hôte via host.docker.internal +docker run --rm -v "${PWD}/load-test/k6:/scripts" grafana/k6 run -e BASE_URL=http://host.docker.internal:8080 /scripts/storefront.js +``` + +Sous Linux, utiliser `--network host` et `BASE_URL=http://127.0.0.1:8080` pour que le conteneur utilise le réseau de l’hôte. + +## Ce que fait le script + +- **storefront.js** + Chaque utilisateur virtuel en boucle : GET `/`, GET `/catalog`, GET `/search?q=test`, avec de courtes pauses aléatoires. + Seuils : <5 % de requêtes en échec, p95 < 5 s. + +Si ton GrandNode utilise d’autres chemins (ex. pas de `/catalog`), adapte le script dans `k6/storefront.js` ou assouplis les checks (ex. accepter 404 pour les pages optionnelles). + +## Résultats et interprétation + +- **Pendant le test** tu vois la progression : VUs (utilisateurs virtuels), temps écoulé, itérations complétées. L’absence d’erreurs dans ce flux signifie que les requêtes partent et que des réponses sont reçues. +- **À la fin** (~6 min pour le scénario 1k) k6 affiche un **résumé** : itérations totales, `http_req_duration` (moyenne, p95, médiane, …), `http_req_failed` (taux), et si les **seuils** sont passés (ex. `http_req_failed` < 5 %, p95 de `http_req_duration` < 5 s). Descends en bas du terminal pour le voir. + +Pour enregistrer le résumé dans un fichier avec Docker, monte un dossier et utilise `--out json` : + +```powershell +# Depuis le dossier load-test ; crée load-test/out et y écrit le résumé +mkdir -Force out +docker run --rm -v ${PWD}:/scripts -v ${PWD}/out:/out grafana/k6 run -e SCENARIO=1k -e BASE_URL=http://host.docker.internal:8080 --out json=/out/summary.json /scripts/k6/storefront.js +``` + +Ensuite ouvre `load-test/out/summary.json` pour les métriques. diff --git a/load-test/k6/storefront.js b/load-test/k6/storefront.js new file mode 100644 index 000000000..42883f1a2 --- /dev/null +++ b/load-test/k6/storefront.js @@ -0,0 +1,114 @@ +/** + * k6 load test – GrandNode storefront + * Simulates browsing: homepage, catalog, product page, search. + * Configure via env: BASE_URL, VUS, DURATION, or SCENARIO=1k|90k|progressive. + * + * Run 1k users (local): k6 run -e SCENARIO=1k storefront.js + * Run progressive 5K->20K->50K: k6 run -e SCENARIO=progressive storefront.js + * Run (local k6): k6 run storefront.js + * With env: k6 run -e BASE_URL=http://127.0.0.1:8080 -e VUS=500 -e DURATION=3m storefront.js + */ + +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +const BASE_URL = __ENV.BASE_URL || 'http://127.0.0.1:8080'; + +// Optional fixed load (overridden by scenarios if present) +const VUS = __ENV.VUS ? parseInt(__ENV.VUS, 10) : 50; +const DURATION = __ENV.DURATION || '2m'; + +const defaultScenario = { + default: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '1m', target: Math.min(VUS, 100) }, + { duration: DURATION, target: VUS }, + { duration: '30s', target: 0 }, + ], + gracefulRampDown: '30s', + gracefulStop: '30s', + startTime: '0s', + }, +}; +// 1k users: for local testing (ramp to 1000 VUs, hold 3m, ramp down) +const scenario1k = { + ramp_1k: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '2m', target: 1000 }, + { duration: '3m', target: 1000 }, + { duration: '1m', target: 0 }, + ], + gracefulRampDown: '30s', + gracefulStop: '30s', + startTime: '0s', + }, +}; + +const scenario90k = { + ramp_90k: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '5m', target: 500 }, + { duration: '20m', target: 2000 }, + { duration: '30m', target: 3000 }, + { duration: '10m', target: 1000 }, + { duration: '2m', target: 0 }, + ], + gracefulRampDown: '1m', + gracefulStop: '30s', + startTime: '0s', + }, +}; + +// Progressive: 5K -> 20K -> 50K users (standard ramp/hold durations) +const scenarioProgressive = { + progressive_5k_20k_50k: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '5m', target: 5000 }, + { duration: '5m', target: 5000 }, + { duration: '10m', target: 20000 }, + { duration: '5m', target: 20000 }, + { duration: '10m', target: 50000 }, + { duration: '5m', target: 50000 }, + { duration: '5m', target: 0 }, + ], + gracefulRampDown: '2m', + gracefulStop: '30s', + startTime: '0s', + }, +}; + +const scenarioMap = { + '1k': scenario1k, + '90k': scenario90k, + progressive: scenarioProgressive, +}; +export const options = { + scenarios: scenarioMap[__ENV.SCENARIO] || defaultScenario, + thresholds: { + http_req_failed: ['rate<0.05'], + http_req_duration: ['p(95)<5000'], + }, +}; + +// (e.g. 90k requests or 90k “user actions” over time) +export default function () { + const res = http.get(`${BASE_URL}/`); + check(res, { 'homepage status 200': (r) => r.status === 200 }); + sleep(0.5 + Math.random() * 1.5); + + const catalog = http.get(`${BASE_URL}/catalog`); + check(catalog, { 'catalog ok': (r) => r && r.status < 500 }); + sleep(0.3 + Math.random() * 1); + + const search = http.get(`${BASE_URL}/search?q=test`); + check(search, { 'search ok': (r) => r && r.status < 500 }); + sleep(0.5 + Math.random() * 2); +} diff --git a/load-test/run-1k.ps1 b/load-test/run-1k.ps1 new file mode 100644 index 000000000..c18fae708 --- /dev/null +++ b/load-test/run-1k.ps1 @@ -0,0 +1,27 @@ +# Scenario 1k users - test local (ramp 2m -> 1000 VUs, palier 3m, ramp down 1m) +# Usage: from load-test folder, .\run-1k.ps1 +# GrandNode must be running: docker compose up -d (from project root) + +$ErrorActionPreference = 'Stop' +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +if ($env:BASE_URL) { + $baseUrl = $env:BASE_URL +} else { + $baseUrl = 'http://127.0.0.1:8080' +} + +if ($env:BASE_URL) { + $baseUrlDocker = $env:BASE_URL +} else { + $baseUrlDocker = 'http://host.docker.internal:8080' +} + +if (Get-Command k6 -ErrorAction SilentlyContinue) { + k6 run -e SCENARIO=1k -e ('BASE_URL=' + $baseUrl) ($scriptDir + '\k6\storefront.js') +} else { + Write-Host ('k6 not installed - running via Docker. BASE_URL=' + $baseUrlDocker) -ForegroundColor Yellow + $volume = $scriptDir + ':/scripts' + $envArg = 'BASE_URL=' + $baseUrlDocker + & docker run --rm -v $volume grafana/k6 run -e SCENARIO=1k -e $envArg /scripts/k6/storefront.js +} diff --git a/load-test/run-90k.ps1 b/load-test/run-90k.ps1 new file mode 100644 index 000000000..ad8286e08 --- /dev/null +++ b/load-test/run-90k.ps1 @@ -0,0 +1,15 @@ +# Run k6 90k-style load test (ramp to 3k VUs over ~1h). +# Usage: from load-test folder, .\run-90k.ps1 +# Ensure GrandNode is up: docker compose up -d (from project root) + +$ErrorActionPreference = "Stop" +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$baseUrl = if ($env:BASE_URL) { $env:BASE_URL } else { "http://127.0.0.1:8080" } +$baseUrlDocker = if ($env:BASE_URL) { $env:BASE_URL } else { "http://host.docker.internal:8080" } + +if (Get-Command k6 -ErrorAction SilentlyContinue) { + k6 run -e SCENARIO=90k -e "BASE_URL=$baseUrl" "$scriptDir\k6\storefront.js" +} else { + Write-Host "k6 not found – running via Docker (BASE_URL=$baseUrlDocker)." -ForegroundColor Yellow + docker run --rm -v "${scriptDir}:/scripts" grafana/k6 run -e SCENARIO=90k -e "BASE_URL=$baseUrlDocker" /scripts/k6/storefront.js +} diff --git a/load-test/run-progressive.ps1 b/load-test/run-progressive.ps1 new file mode 100644 index 000000000..363858932 --- /dev/null +++ b/load-test/run-progressive.ps1 @@ -0,0 +1,27 @@ +# Scénario progressif 5K -> 20K -> 50K (durée ~45 min) +# Usage : depuis le dossier load-test, .\run-progressive.ps1 +# GrandNode doit tourner : docker compose up -d (à la racine du projet) + +$ErrorActionPreference = 'Stop' +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +if ($env:BASE_URL) { + $baseUrl = $env:BASE_URL +} else { + $baseUrl = 'http://127.0.0.1:8080' +} + +if ($env:BASE_URL) { + $baseUrlDocker = $env:BASE_URL +} else { + $baseUrlDocker = 'http://host.docker.internal:8080' +} + +if (Get-Command k6 -ErrorAction SilentlyContinue) { + k6 run -e SCENARIO=progressive -e ('BASE_URL=' + $baseUrl) ($scriptDir + '\k6\storefront.js') +} else { + Write-Host ('k6 non installé – lancement via Docker. BASE_URL=' + $baseUrlDocker) -ForegroundColor Yellow + $volume = $scriptDir + ':/scripts' + $envArg = 'BASE_URL=' + $baseUrlDocker + & docker run --rm -v $volume grafana/k6 run -e SCENARIO=progressive -e $envArg /scripts/k6/storefront.js +}