diff --git a/docs/handbook/10-deploy-azure-free.md b/docs/handbook/10-deploy-azure-free.md index 69ceb7f..1e8aa56 100644 --- a/docs/handbook/10-deploy-azure-free.md +++ b/docs/handbook/10-deploy-azure-free.md @@ -264,9 +264,28 @@ az webapp config appsettings set --name $APP --resource-group $RG --settings \ Jwt__Issuer="https://$APP.azurewebsites.net" \ Jwt__Audience="widgetworks-spa" \ Payments__Provider="Mock" \ - Email__Provider="Dev" + Email__Provider="Dev" \ + RateLimiting__TrustForwardedFor="true" ``` +`RateLimiting__TrustForwardedFor` is not optional here. App Service is a reverse proxy, so every +request reaches the app carrying the *proxy's* address. Left `false`, every caller in the world +collapses into a single throttling partition and the per-caller limits become a global cap that the +first busy minute trips for everybody. The app logs a warning once when the setting and the traffic +disagree — if you see it, this is the line you missed. See +[throttling](04-configuration-and-2fa.md#throttling-and-the-one-setting-that-can-cause-an-outage). + +**Health probe.** On Basic or higher, point the platform at readiness rather than liveness: + +```bash +az webapp config set --name $APP --resource-group $RG --health-check-path /health/ready +``` + +Health check is unavailable on **F1/Free** — the setting simply will not apply. There, use an +external monitor (or a scheduled workflow) against `/health/ready` every 30–60 minutes. Not more +often: readiness queries the database, and waking it on the keep-warm cadence is exactly the cost +`/health` exists to avoid. + Verify every reference resolved — a broken one shows an error instead of `Resolved`: ```bash diff --git a/infra/Provision.ps1 b/infra/Provision.ps1 index bc368b6..5e16567 100644 --- a/infra/Provision.ps1 +++ b/infra/Provision.ps1 @@ -353,13 +353,42 @@ Write-Ok "SPA deployed to $SwaUrl" # ---------------------------------------------------------- 10. CORS loop ---- # Named origin, never a wildcard - the app sends credentials. App__BaseUrl is what password-reset # emails build their links from; left at localhost it sends customers to their own machine. -Write-Step 'Wiring CORS and email links' +# +# RateLimiting__TrustForwardedFor rides along here, in the block that runs on every provision, +# rather than in section 6 which -SkipInfra skips. It is a correctness setting, not an +# infrastructure one: App Service is a reverse proxy, so every request reaches the app carrying +# the proxy's address. Left false, all callers collapse into one throttling partition and the +# per-caller limits become a global cap that the first busy minute trips for everybody. The app +# logs a warning when the setting and the traffic disagree, but a provision run should not +# produce an environment that needs the warning. +Write-Step 'Wiring CORS, email links, and proxy-aware throttling' Set-AppSettings -App $AppName -Group $ResourceGroup -Settings @{ - 'Cors__AllowedOrigins' = $SwaUrl - 'App__BaseUrl' = $SwaUrl + 'Cors__AllowedOrigins' = $SwaUrl + 'App__BaseUrl' = $SwaUrl + 'RateLimiting__TrustForwardedFor' = 'true' } Write-Ok 'Set' +# ------------------------------------------------------- 11. health probe ---- +# /health is liveness and deliberately does not touch the database - the keep-warm schedule pings +# it every few minutes, and waking a serverless database on that cadence would cost far more than +# the free allowance. /health/ready is the one that answers "can this instance actually serve", +# so that is what the platform should watch. +# +# Health check needs Basic or higher; on the F1 free tier the setting does not apply. Rather than +# fail a free-tier provision, say so and point at the substitute. +Write-Step 'Health check path' +if ($Sku -eq 'F1' -or $Sku -eq 'D1') { + Write-Warn "Health check is not available on the $Sku tier - skipping." + Write-Host ' Substitute: point an external monitor (or a scheduled workflow) at' -ForegroundColor DarkGray + Write-Host " $ApiUrl/health/ready on a 30-60 minute interval. Not more often:" -ForegroundColor DarkGray + Write-Host ' a readiness check wakes the database, which is what /health avoids.' -ForegroundColor DarkGray +} else { + Invoke-Az webapp config set --name $AppName --resource-group $ResourceGroup ` + --health-check-path '/health/ready' --output none | Out-Null + Write-Ok 'Probing /health/ready (readiness, queries the database)' +} + Write-Host @" Done