diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 0000000..0b5a2f5 --- /dev/null +++ b/README.fr.md @@ -0,0 +1,195 @@ + +# Atelier App Insights .NET 10 — MAPAQ + +> 🇫🇷 **Français (ce fichier)** · 🇬🇧 **[Read me in English](README.md)** + +Atelier public bilingue (FR par défaut / EN en parallèle) d'environ 2 heures qui illustre le traçage distribué Application Insights de bout en bout sur **navigateur → ASP.NET Core 10 Razor Pages → API minimale → EF Core / Azure SQL**, autour des données ouvertes du Ministère de l'Agriculture, des Pêcheries et de l'Alimentation du Québec (MAPAQ). + +| | | +| --- | --- | +| **Site de l'atelier (FR par défaut)** | | +| **Site de l'atelier (EN)** | | +| **IC** | [![ci](https://github.com/devopsabcs-engineering/app-insights-dotnet/actions/workflows/ci.yml/badge.svg)](.github/workflows/ci.yml) | +| **Déploiement** | [![deploy](https://github.com/devopsabcs-engineering/app-insights-dotnet/actions/workflows/deploy.yml/badge.svg)](.github/workflows/deploy.yml) | +| **Pages** | [![pages](https://github.com/devopsabcs-engineering/app-insights-dotnet/actions/workflows/pages.yml/badge.svg)](.github/workflows/pages.yml) | +| **Diapos** | [![build-decks](https://github.com/devopsabcs-engineering/app-insights-dotnet/actions/workflows/build-decks.yml/badge.svg)](.github/workflows/build-decks.yml) | +| **Vérification des liens** | [![link-check](https://github.com/devopsabcs-engineering/app-insights-dotnet/actions/workflows/link-check.yml/badge.svg)](.github/workflows/link-check.yml) | +| **Licence** | [MIT](LICENSE) | + +--- + +## Ce que vous bâtirez + +* Deux App Services ASP.NET Core 10 sous Linux — `Mapaq.Web` (Razor Pages, localisation FR par défaut, fragment JS Application Insights) et `Mapaq.Api` (API minimale + EF Core). +* Une base Azure SQL (sans serveur `GP_S_Gen5_1`, mise en pause automatique, **authentification Entra uniquement via une identité gérée affectée par l'utilisateur (UAMI)** placée dans un groupe Entra qui détient `db_owner`). +* Une ressource Application Insights basée sur un espace de travail Log Analytics, qui capture une trace distribuée par clic de l'interface — `pageView` du navigateur, requête serveur, dépendance API et dépendance SQL partagent toutes un même `operation_Id`. +* Un réseau privé : VNet + délégation de sous-réseau pour l'intégration VNet d'App Service + points de terminaison privés pour SQL. +* Un cycle de vie reproductible `azd up` / `azd down` avec un plafond de coût strict de **≤ 0,60 $ US par participant et par session de 2 heures**. + +## Architecture + +```mermaid +flowchart LR + user([👤 Navigateur du participant]) + subgraph azure["Abonnement Azure · rg-${env}"] + direction LR + web["Mapaq.Web
Razor Pages"] + api["Mapaq.Api
API minimale"] + sql[("Azure SQL
sans serveur")] + kv[("Key Vault")] + uami["UAMI
idy***"] + ai[("Application
Insights")] + law[("Espace de travail
Log Analytics")] + end + user -- HTTPS + traceparent --> web + web -- HttpClient + contexte de trace W3C --> api + api -- EF Core / SqlClient --> sql + web -.identité fédérée.-> uami + api -.identité fédérée.-> uami + uami -.membre du groupe Entra
= admin SQL.-> sql + web -- distro OTel --> ai + api -- distro OTel --> ai + ai --> law +``` + +## Pile technique + +| Couche | Choix | Notes | +| --- | --- | --- | +| Exécution | **.NET 10** (`10.0.100`, `rollForward: latestFeature`) | Épinglée dans [global.json](global.json) | +| Web | ASP.NET Core 10 Razor Pages + `Microsoft.Extensions.Localization` 10.0.0 | FR par défaut, EN bascule via `/setlang` | +| API | API minimale ASP.NET Core 10 + `Microsoft.AspNetCore.OpenApi` 10.0.0 + Swagger UI 7.2 | OpenAPI à `/openapi/v1.json`, IU à `/swagger` | +| Données | EF Core 10 + `Microsoft.Data.SqlClient` 6.1.1 | Repli en mémoire si aucune chaîne `MapaqSql` n'est fournie | +| Télémétrie | **Distro Azure Monitor OpenTelemetry** `Azure.Monitor.OpenTelemetry.AspNetCore` 1.4.0 | `SamplingRatio = 1.0F`, `TracesPerSecond = null` (intention pédagogique : capturer chaque trace) | +| Identité | `Azure.Identity` 1.14.2, `Microsoft.Identity.Web` 3.5.0 | UAMI injectée dans la chaîne SQL via `User Id={uamiClientId}` | +| Tests | xUnit 2.9 + `Microsoft.AspNetCore.Mvc.Testing` 10.0 (unitaires) · Locust (charge) · Playwright (IU) | Voir [`tests/`](tests/) | +| Infra | Bicep (portée abonnement) + `azd` | Voir [`infra/`](infra/) et [azure.yaml](azure.yaml) | + +## Démarrage rapide + +```pwsh +azd auth login +azd up # provisionne l'infra + déploie les deux apps +# essayez la démo à l'URL WEB_URI affichée +azd down --purge --force # supprime le RG + purge KV/LAW supprimés en douceur +``` + +`azd up` exécute le hook post-provisionnement [`infra/scripts/grant-sql-access.{sh,ps1}`](infra/scripts/) qui ajoute le principal de l'UAMI au groupe Entra admin SQL pour que l'API puisse s'authentifier auprès d'Azure SQL. (En IC, la même étape est réalisée par le workflow lui-même — voir [Déploiement depuis l'IC](#déploiement-depuis-lic).) + +## Développement local + +Les applications de référence fonctionnent de bout en bout sans aucune ressource Azure grâce au repli EF Core en mémoire dans `Mapaq.Api`. + +```pwsh +pwsh ./scripts/start-local.ps1 # construit + lance les deux apps en arrière-plan +# Mapaq.Web → https://localhost:7010 +# Mapaq.Api → https://localhost:7020 (OpenAPI à /openapi/v1.json, Swagger à /swagger) +pwsh ./scripts/stop-local.ps1 # arrête les processus dotnet +``` + +Brancher la véritable télémétrie en local : + +```pwsh +$cs = az monitor app-insights component show -g rg-dev-001 -a aiy*** --query connectionString -o tsv +pwsh ./scripts/start-local.ps1 -ConnectionString $cs +``` + +Voir [scripts/README.md](scripts/README.md) pour les options additionnelles (vraie base Azure SQL, origines personnalisées, etc.). + +## Tests + +```pwsh +# unitaires + intégration (Mapaq.Api.Tests, Mapaq.Web.Tests) +dotnet build Mapaq.sln /warnaserror +dotnet test Mapaq.sln + +# charge (Locust sans interface, 25 utilisateurs virtuels, 2 min, ouvre le rapport HTML) +pwsh ./scripts/load-test.ps1 + +# IU (Playwright) +pwsh ./scripts/run-ui-tests.ps1 +``` + +* Les tests de charge se trouvent sous [`tests/load/`](tests/load/) — voir [tests/load/README.md](tests/load/README.md). +* Les spécifications IU se trouvent sous [`tests/ui/specs/`](tests/ui/) et sont câblées dans le [workflow ui-tests](.github/workflows/ui-tests.yml). + +## Cartographie du dépôt + +| Chemin | Rôle | +| --- | --- | +| [`src/Mapaq.Web/`](src/Mapaq.Web/) | Front-end Razor Pages, localisation FR par défaut, fragment JS App Insights | +| [`src/Mapaq.Api/`](src/Mapaq.Api/) | API minimale (`/api/establishments`, `/api/establishments/{id}`, `/api/inspections/rollup`, `/api/sync`) | +| [`src/Mapaq.Domain/`](src/Mapaq.Domain/) | Entités simples (`Establishment`, `Conviction`, `Suspension`, `InspectionRollup`, `SyncJob`) | +| [`src/Mapaq.Infrastructure/`](src/Mapaq.Infrastructure/) | `MapaqDbContext`, configurations EF, `MapaqDemoSeeder`, `SeedLoader` | +| [`tests/Mapaq.Api.Tests/`](tests/Mapaq.Api.Tests/) · [`tests/Mapaq.Web.Tests/`](tests/Mapaq.Web.Tests/) | Tests d'intégration xUnit + `WebApplicationFactory` | +| [`tests/load/`](tests/load/) | Tests de charge Locust | +| [`tests/ui/`](tests/ui/) | Tests IU Playwright + publication des captures d'écran | +| [`infra/main.bicep`](infra/main.bicep) | Orchestrateur à portée abonnement | +| [`infra/modules/`](infra/modules/) | `loganalytics`, `appinsights`, `identity`, `keyvault`, `vnet`, `sql`, `privateEndpoints`, `appservice`, `roleAssignments` | +| [`infra/scripts/`](infra/scripts/) | Hooks post-provisionnement `azd` (octroi par appartenance de groupe + assistant de credentials fédérés) | +| [`.github/workflows/`](.github/workflows/) | `ci`, `deploy`, `teardown`, `pages`, `build-decks`, `link-check`, `markdown-lint`, `load-test`, `ui-tests`, `seed-ado-boards` | +| [`.azuredevops/pipelines/`](.azuredevops/pipelines/) | `ci`, `deploy`, `teardown`, `load-test`, `ui-tests`, `adv-sec` (Microsoft Defender pour DevOps) | +| [`boards/`](boards/) | Backlog bilingue ADO Boards ([work-items.yaml](boards/work-items.yaml)) + [seed-ado-boards.ps1](boards/seed-ado-boards.ps1) | +| [`labs/`](labs/) · [`fr/labs/`](fr/labs/) | Contenu des ateliers rendu en site Jekyll | +| [`slides/`](slides/) | Diapos HTML bilingues façon Reveal + générateur PPTX ([content/en/](slides/content/en/), [content/fr/](slides/content/fr/)) | +| [`docs/`](docs/) | Diapos HTML pré-construites servies depuis Pages ([EN](docs/app-insights-dotnet.html), [FR](docs/app-insights-dotnet-fr.html)) | +| [`data/seed/`](data/seed/) | Échantillons CSV synthétiques dérivés des jeux Données Québec | +| [`scripts/`](scripts/) | `start-local`, `stop-local`, `load-test`, `run-load-test`, `run-ui-tests` | +| [`.devcontainer/`](.devcontainer/) | Définition Codespaces / Dev Containers (.NET 10, Node 20, Python 3.12, az/azd/gh/pwsh, Ruby 3.3 pour Jekyll) | + +## Contenu de l'atelier + +Sept ateliers séquentiels (~2 heures au total). Choisissez votre langue : + +| # | FR | EN | Durée | +| --- | --- | --- | --- | +| 00 | [Installation](fr/labs/lab-00-installation.md) | [Setup](labs/lab-00-setup.md) | 15 min | +| 01 | [Provisionnement Azure](fr/labs/lab-01-provisionnement.md) | [Provision Azure infra](labs/lab-01-provision.md) | 15 min | +| 02 | [Instrumentation web](fr/labs/lab-02-instrumentation-web.md) | [Instrument the web tier](labs/lab-02-instrument-web.md) | 20 min | +| 03 | [Instrumentation API + SQL](fr/labs/lab-03-instrumentation-api-sql.md) | [Instrument API + SQL](labs/lab-03-instrument-api-sql.md) | 20 min | +| 04 | [Corrélation navigateur](fr/labs/lab-04-correlation-navigateur.md) | [Browser ↔ server correlation](labs/lab-04-browser-correlation.md) | 15 min | +| 05 | [Tableaux de bord](fr/labs/lab-05-tableaux-de-bord.md) | [Dashboards, KQL, alerts](labs/lab-05-dashboards.md) | 20 min | +| 06 | [Démantèlement](fr/labs/lab-06-demantelement.md) | [Teardown](labs/lab-06-teardown.md) | 15 min | + +## Infrastructure + +`infra/main.bicep` est un orchestrateur **à portée abonnement** qui crée `rg-${environmentName}` et appelle neuf modules. Paramètres requis : + +| Paramètre | Source | +| --- | --- | +| `environmentName` | `azd env new ` | +| `location` | par défaut `canadacentral` | +| `sqlAdminPrincipalId` | identifiant d'objet de l'utilisateur ou groupe Entra qui devient admin SQL | +| `sqlAdminLogin` | nom d'affichage visible dans le portail | +| `sqlAdminPrincipalType` | `User` ou `Group` (par défaut `Group`) | + +Sorties (consommées par `azd env`, les scripts post-provisionnement et l'IC) : + +`WEB_URI`, `API_URI`, `SQL_FQDN`, `KV_NAME`, `APPINSIGHTS_CONNECTION_STRING`, `AZURE_RESOURCE_GROUP`, `AZURE_LOCATION`, `AZURE_CLIENT_ID`, `RESOURCE_TOKEN`, `SQL_DATABASE_NAME`, `UAMI_NAME`, `UAMI_PRINCIPAL_ID`. + +## Déploiement depuis l'IC + +Deux pipelines d'IC/CD parallèles existent — choisissez celui qui correspond à votre plateforme : + +* **GitHub Actions** ([.github/workflows/deploy.yml](.github/workflows/deploy.yml)) — connexion fédérée OIDC, contrôlée par l'environnement GitHub `workshop-dev`. +* **Azure DevOps Pipelines** ([.azuredevops/pipelines/deploy.yml](.azuredevops/pipelines/deploy.yml)) — fédération d'identité de charge de travail, mêmes étapes logiques. + +Les deux workflows exécutent la même séquence post-provisionnement : + +1. `azd up` (provisionnement + déploiement). +2. **Ajout de l'UAMI au groupe Entra admin SQL** — échoue rapidement sur toute erreur autre que « déjà membre » afin qu'un octroi cassé ne puisse pas passer silencieusement l'IC. +3. **Redémarrage de tous les App Services `mapaq-*`** — vide les jetons du pool de connexions `SqlClient` capturés avant l'ajout de l'UAMI au groupe, pour que la première requête après déploiement ne renvoie pas un 500 avec `Login failed for user ''`. + +Les workflows de démantèlement ([GHA](.github/workflows/teardown.yml) / [ADO](.azuredevops/pipelines/teardown.yml)) suppriment le groupe de ressources avec `az` (et non `azd down` — l'état azd vit sur le runner qui a exécuté `azd up`, qui n'existe jamais sur un runner d'IC neuf) puis purgent Key Vault et Log Analytics supprimés en douceur pour que le même nom d'environnement puisse être reprovisionné sans collision. + +## Règle de parité bilingue + +Le contenu est rédigé d'abord en **anglais**, mais le **français est la langue publiée par défaut**. Toute modification d'un atelier, d'une diapositive ou d'un fichier de prose de premier niveau doit mettre à jour les deux langues — voir [CONTRIBUTING.md](CONTRIBUTING.md). + +## Licence + +[MIT](LICENSE) + +## Contribuer + +Voir [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/README.md b/README.md index ed8478d..4219873 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,190 @@ # App Insights .NET 10 — MAPAQ Workshop -A public bilingual (FR-primary / EN-parallel) 2-hour workshop demonstrating end-to-end Application Insights distributed tracing across **browser → ASP.NET Core 10 Razor Pages → Minimal API → Azure SQL**, themed around open data from the Ministère de l'Agriculture, des Pêcheries et de l'Alimentation du Québec (MAPAQ). +> 🇬🇧 **English (this file)** · 🇫🇷 **[Lisez-moi en français](README.fr.md)** -> **Workshop site (FR default):** -> **Workshop site (EN):** +A public bilingual (FR-default / EN-parallel) ~2-hour workshop that demonstrates end-to-end Application Insights distributed tracing across **browser → ASP.NET Core 10 Razor Pages → Minimal API → EF Core / Azure SQL**, themed around open data from the Ministère de l'Agriculture, des Pêcheries et de l'Alimentation du Québec (MAPAQ). + +| | | +| --- | --- | +| **Workshop site (FR default)** | | +| **Workshop site (EN)** | | +| **CI** | [![ci](https://github.com/devopsabcs-engineering/app-insights-dotnet/actions/workflows/ci.yml/badge.svg)](.github/workflows/ci.yml) | +| **Deploy** | [![deploy](https://github.com/devopsabcs-engineering/app-insights-dotnet/actions/workflows/deploy.yml/badge.svg)](.github/workflows/deploy.yml) | +| **Pages** | [![pages](https://github.com/devopsabcs-engineering/app-insights-dotnet/actions/workflows/pages.yml/badge.svg)](.github/workflows/pages.yml) | +| **Decks** | [![build-decks](https://github.com/devopsabcs-engineering/app-insights-dotnet/actions/workflows/build-decks.yml/badge.svg)](.github/workflows/build-decks.yml) | +| **Link check** | [![link-check](https://github.com/devopsabcs-engineering/app-insights-dotnet/actions/workflows/link-check.yml/badge.svg)](.github/workflows/link-check.yml) | +| **License** | [MIT](LICENSE) | + +--- ## What you'll build -* Two ASP.NET Core 10 Linux App Services (`Mapaq.Web` Razor Pages + `Mapaq.Api` Minimal API). -* An Azure SQL Database (serverless `GP_S_Gen5_1` with auto-pause, Entra-only auth). +* Two ASP.NET Core 10 Linux App Services — `Mapaq.Web` (Razor Pages, FR-default localization, JS Application Insights snippet) and `Mapaq.Api` (Minimal API + EF Core). +* An Azure SQL Database (serverless `GP_S_Gen5_1`, auto-pause, **Entra-only auth via a User-Assigned Managed Identity** in an Entra group that holds `db_owner`). * A workspace-based Application Insights resource that captures one distributed trace per UI click — browser pageView, server request, downstream API dependency, and SQL dependency all share a single `operation_Id`. +* A private network: VNet + subnet delegation for App Service VNet integration + private endpoints for SQL. +* A reproducible `azd up` / `azd down` lifecycle with a hard cost ceiling of **≤ $0.60 USD per attendee per 2-hour run**. + +## Architecture + +```mermaid +flowchart LR + user([👤 Attendee browser]) + subgraph azure["Azure subscription · rg-${env}"] + direction LR + web["Mapaq.Web
Razor Pages"] + api["Mapaq.Api
Minimal API"] + sql[("Azure SQL
serverless")] + kv[("Key Vault")] + uami["UAMI
idy***"] + ai[("Application
Insights")] + law[("Log Analytics
workspace")] + end + user -- HTTPS + traceparent --> web + web -- HttpClient + W3C trace context --> api + api -- EF Core / SqlClient --> sql + web -.federated identity.-> uami + api -.federated identity.-> uami + uami -.member of Entra group
= SQL admin.-> sql + web -- OTel distro --> ai + api -- OTel distro --> ai + ai --> law +``` + +## Tech stack + +| Layer | Choice | Notes | +| --- | --- | --- | +| Runtime | **.NET 10** (`10.0.100`, `rollForward: latestFeature`) | Pinned in [global.json](global.json) | +| Web | ASP.NET Core 10 Razor Pages + `Microsoft.Extensions.Localization` 10.0.0 | FR-default, EN switchable via `/setlang` | +| API | ASP.NET Core 10 Minimal API + `Microsoft.AspNetCore.OpenApi` 10.0.0 + Swagger UI 7.2 | OpenAPI at `/openapi/v1.json`, UI at `/swagger` | +| Data | EF Core 10 + `Microsoft.Data.SqlClient` 6.1.1 | In-memory fallback when no `MapaqSql` connection string is supplied | +| Telemetry | **Azure Monitor OpenTelemetry Distro** `Azure.Monitor.OpenTelemetry.AspNetCore` 1.4.0 | `SamplingRatio = 1.0F`, `TracesPerSecond = null` (workshop intent: capture every trace) | +| Identity | `Azure.Identity` 1.14.2, `Microsoft.Identity.Web` 3.5.0 | UAMI baked into SQL connection string via `User Id={uamiClientId}` | +| Tests | xUnit 2.9 + `Microsoft.AspNetCore.Mvc.Testing` 10.0 (unit) · Locust (load) · Playwright (UI) | See [`tests/`](tests/) | +| Infra | Bicep (subscription scope) + `azd` | See [`infra/`](infra/) and [azure.yaml](azure.yaml) | ## Quick start ```pwsh azd auth login -azd up +azd up # provisions infra + deploys both apps # play with the demo at the printed WEB_URI -azd down --purge --force +azd down --purge --force # tears down rg + purges soft-deleted KV/LAW ``` -Cost target: **≤ $0.60 USD per attendee per 2-hour run** (B1 plan + serverless SQL with auto-pause). +`azd up` runs the [`infra/scripts/grant-sql-access.{sh,ps1}`](infra/scripts/) postprovision hook that adds the UAMI principal to the SQL admin Entra group so the API can authenticate to Azure SQL. (In CI the same step is performed by the workflow itself — see [Deploying from CI](#deploying-from-ci).) -## Repository navigation +## Local development -| Area | EN | FR | -| --- | --- | --- | -| Workshop site (Jekyll) | [`labs/`](labs/) | [`fr/labs/`](fr/labs/) | -| Demo solution | [`src/`](src/) | — | -| Infrastructure | [`infra/`](infra/) | — | -| GitHub Actions | [`.github/workflows/`](.github/workflows/) | — | -| Azure DevOps Pipelines | [`azure-pipelines/`](azure-pipelines/) | — | -| ADO Boards seed | [`boards/`](boards/) | [`boards/README.md`](boards/README.md) | -| HTML + PPTX decks | [`docs/`](docs/) | [`docs/`](docs/) | -| Open-data snapshots | [`data/seed/`](data/seed/) | [`data/seed/`](data/seed/) | - -## En français - -Cet atelier public bilingue de 2 heures démontre la corrélation Application Insights de bout en bout entre le navigateur, ASP.NET Core 10 (Razor Pages), une API Minimal et Azure SQL, autour des données ouvertes du MAPAQ. La langue par défaut publiée est le français ; un commutateur permet de basculer en anglais. - -* **Site de l'atelier (par défaut FR) :** -* **Démarrage rapide :** `azd up` puis `azd down --purge --force` (cible de coût ≤ 0,60 $ US par participant). -* **Modules :** `fr/labs/lab-00-installation` à `fr/labs/lab-06-demantelement`. +The reference apps run end-to-end without any Azure resources thanks to the EF Core in-memory fallback in `Mapaq.Api`. + +```pwsh +pwsh ./scripts/start-local.ps1 # builds + launches both apps in background pwsh windows +# Mapaq.Web → https://localhost:7010 +# Mapaq.Api → https://localhost:7020 (OpenAPI at /openapi/v1.json, Swagger at /swagger) +pwsh ./scripts/stop-local.ps1 # kills the dotnet processes +``` + +Wire up real telemetry locally: + +```pwsh +$cs = az monitor app-insights component show -g rg-dev-001 -a aiy*** --query connectionString -o tsv +pwsh ./scripts/start-local.ps1 -ConnectionString $cs +``` + +See [scripts/README.md](scripts/README.md) for additional flags (real Azure SQL, custom origins, etc.). + +## Tests + +```pwsh +# unit + integration (Mapaq.Api.Tests, Mapaq.Web.Tests) +dotnet build Mapaq.sln /warnaserror +dotnet test Mapaq.sln + +# load (Locust headless, 25 VU, 2 min, opens HTML report) +pwsh ./scripts/load-test.ps1 + +# UI (Playwright) +pwsh ./scripts/run-ui-tests.ps1 +``` + +* Load tests live in [`tests/load/`](tests/load/) — see [tests/load/README.md](tests/load/README.md). +* UI specs live in [`tests/ui/specs/`](tests/ui/) and are wired into the [ui-tests workflow](.github/workflows/ui-tests.yml). + +## Repository layout + +| Path | Purpose | +| --- | --- | +| [`src/Mapaq.Web/`](src/Mapaq.Web/) | Razor Pages front-end, FR-default localization, JS App Insights loader snippet | +| [`src/Mapaq.Api/`](src/Mapaq.Api/) | Minimal API (`/api/establishments`, `/api/establishments/{id}`, `/api/inspections/rollup`, `/api/sync`) | +| [`src/Mapaq.Domain/`](src/Mapaq.Domain/) | Plain entities (`Establishment`, `Conviction`, `Suspension`, `InspectionRollup`, `SyncJob`) | +| [`src/Mapaq.Infrastructure/`](src/Mapaq.Infrastructure/) | `MapaqDbContext`, EF configurations, `MapaqDemoSeeder`, `SeedLoader` | +| [`tests/Mapaq.Api.Tests/`](tests/Mapaq.Api.Tests/) · [`tests/Mapaq.Web.Tests/`](tests/Mapaq.Web.Tests/) | xUnit + `WebApplicationFactory` integration tests | +| [`tests/load/`](tests/load/) | Locust load tests | +| [`tests/ui/`](tests/ui/) | Playwright UI tests + screenshot publisher | +| [`infra/main.bicep`](infra/main.bicep) | Subscription-scope orchestrator | +| [`infra/modules/`](infra/modules/) | `loganalytics`, `appinsights`, `identity`, `keyvault`, `vnet`, `sql`, `privateEndpoints`, `appservice`, `roleAssignments` | +| [`infra/scripts/`](infra/scripts/) | `azd` postprovision hooks (group-membership grant + federated credential helper) | +| [`.github/workflows/`](.github/workflows/) | `ci`, `deploy`, `teardown`, `pages`, `build-decks`, `link-check`, `markdown-lint`, `load-test`, `ui-tests`, `seed-ado-boards` | +| [`.azuredevops/pipelines/`](.azuredevops/pipelines/) | `ci`, `deploy`, `teardown`, `load-test`, `ui-tests`, `adv-sec` (Microsoft Defender for DevOps) | +| [`boards/`](boards/) | Bilingual ADO Boards backlog ([work-items.yaml](boards/work-items.yaml)) + [seed-ado-boards.ps1](boards/seed-ado-boards.ps1) | +| [`labs/`](labs/) · [`fr/labs/`](fr/labs/) | Lab content rendered as a Jekyll site | +| [`slides/`](slides/) | Bilingual reveal-style HTML decks + PPTX builder ([content/en/](slides/content/en/), [content/fr/](slides/content/fr/)) | +| [`docs/`](docs/) | Pre-built HTML decks served from Pages ([EN](docs/app-insights-dotnet.html), [FR](docs/app-insights-dotnet-fr.html)) | +| [`data/seed/`](data/seed/) | Synthetic CSV seed snapshots derived from Données Québec datasets | +| [`scripts/`](scripts/) | `start-local`, `stop-local`, `load-test`, `run-load-test`, `run-ui-tests` | +| [`.devcontainer/`](.devcontainer/) | Codespaces / Dev Containers definition (.NET 10, Node 20, Python 3.12, az/azd/gh/pwsh, Ruby 3.3 for Jekyll) | + +## Workshop content + +Seven sequential labs (~2 hours total). Pick your language: + +| # | EN | FR | Timebox | +| --- | --- | --- | --- | +| 00 | [Setup](labs/lab-00-setup.md) | [Installation](fr/labs/lab-00-installation.md) | 15 min | +| 01 | [Provision Azure infra](labs/lab-01-provision.md) | [Provisionnement Azure](fr/labs/lab-01-provisionnement.md) | 15 min | +| 02 | [Instrument the web tier](labs/lab-02-instrument-web.md) | [Instrumentation web](fr/labs/lab-02-instrumentation-web.md) | 20 min | +| 03 | [Instrument API + SQL](labs/lab-03-instrument-api-sql.md) | [Instrumentation API + SQL](fr/labs/lab-03-instrumentation-api-sql.md) | 20 min | +| 04 | [Browser ↔ server correlation](labs/lab-04-browser-correlation.md) | [Corrélation navigateur](fr/labs/lab-04-correlation-navigateur.md) | 15 min | +| 05 | [Dashboards, KQL, alerts](labs/lab-05-dashboards.md) | [Tableaux de bord](fr/labs/lab-05-tableaux-de-bord.md) | 20 min | +| 06 | [Teardown](labs/lab-06-teardown.md) | [Démantèlement](fr/labs/lab-06-demantelement.md) | 15 min | + +## Infrastructure + +`infra/main.bicep` is a **subscription-scope** orchestrator that creates `rg-${environmentName}` and dispatches nine modules. Required parameters: + +| Parameter | Source | +| --- | --- | +| `environmentName` | `azd env new ` | +| `location` | default `canadacentral` | +| `sqlAdminPrincipalId` | object id of the Entra user/group that becomes SQL admin | +| `sqlAdminLogin` | display name shown in the portal | +| `sqlAdminPrincipalType` | `User` or `Group` (default `Group`) | + +Outputs (consumed by `azd env`, postprovision scripts, and CI): + +`WEB_URI`, `API_URI`, `SQL_FQDN`, `KV_NAME`, `APPINSIGHTS_CONNECTION_STRING`, `AZURE_RESOURCE_GROUP`, `AZURE_LOCATION`, `AZURE_CLIENT_ID`, `RESOURCE_TOKEN`, `SQL_DATABASE_NAME`, `UAMI_NAME`, `UAMI_PRINCIPAL_ID`. + +## Deploying from CI + +Two parallel CI/CD pipelines exist — pick the one matching your platform: + +* **GitHub Actions** ([.github/workflows/deploy.yml](.github/workflows/deploy.yml)) — OIDC federated login, gated on the `workshop-dev` GitHub Environment. +* **Azure DevOps Pipelines** ([.azuredevops/pipelines/deploy.yml](.azuredevops/pipelines/deploy.yml)) — workload-identity federation, same logical steps. + +Both workflows run the same post-provision sequence: + +1. `azd up` (provision + deploy). +2. **Add UAMI to the SQL admin Entra group** — fails fast on any error other than "already a member" so a broken grant cannot silently pass CI. +3. **Restart all `mapaq-*` App Services** — drops the `SqlClient` connection-pool tokens captured before the UAMI was added to the group, so the first request after deploy does not 500 with `Login failed for user ''`. + +The teardown workflows ([GHA](.github/workflows/teardown.yml) / [ADO](.azuredevops/pipelines/teardown.yml)) delete the resource group with `az` (not `azd down` — azd state lives on the runner that ran `azd up`, which never exists on a fresh CI runner) and purge soft-deleted Key Vault + Log Analytics so the same env name can be re-provisioned without collisions. + +## Bilingual parity rule + +Content is authored in **English first** but **French is the published default** language. Every change to a lab, deck slide, or top-level prose file must update both languages — see [CONTRIBUTING.md](CONTRIBUTING.md). ## License