End-to-end guide to deploy News Aggregator to Azure: PostgreSQL, Key Vault, API, timer function, and React UI.
flowchart TB
subgraph github [GitHub Actions on main]
infra[azure-deployment.yml]
web[newsagg-web.yml]
func[newsagg-func.yml]
ui[newsagg-ui.yml]
end
subgraph azure [Azure resources - rg-newsagg-prod]
pg[(PostgreSQL 17 Flexible)]
kv[Key Vault]
acr[Container Registry]
webApp[newsagg-web App Service]
funcApp[newsagg-func Function App]
uiApp[newsagg-ui App Service]
end
users[Users] --> uiApp
uiApp -->|"/api proxy"| webApp
webApp --> pg
webApp --> kv
funcApp -->|timer POST /api/scraper/refresh| webApp
ui --> acr
ui --> uiApp
infra --> pg
infra --> kv
infra --> acr
infra --> webApp
infra --> funcApp
infra --> uiApp
web --> webApp
func --> funcApp
| Layer | Azure resource | Deployed by |
|---|---|---|
| Database | PostgreSQL Flexible Server (newsaggpg) |
azure-deployment / Bicep |
| Secrets | Key Vault (newsaggkv) |
Bicep |
| Text analytics | Azure AI Language (newsagg-lang) |
Bicep |
| Backend API | App Service newsagg-web (.NET 10) |
newsagg-web.yml |
| Scraper timer | Function App newsagg-func (Python 3.13) |
newsagg-func.yml |
| Frontend | App Service newsagg-ui (nginx + React container) |
newsagg-ui.yml |
| Images | ACR newsaggacr |
Bicep + newsagg-ui.yml |
The Bicep template provisions an Azure AI Language (Text Analytics) account, stores its access key in Key Vault as AzureLanguageKey, and surfaces AzureLanguage__Endpoint/AzureLanguage__Key to the API and ANALYZER_PROVIDER/AZURE_LANGUAGE_ENDPOINT/AZURE_LANGUAGE_KEY to the scraper Function App. The optional huggingFaceToken parameter is stored as the HuggingFaceToken secret and surfaced to the Function App as HF_TOKEN. The required apiSecurityKey parameter is stored as the ApiSecurityKey secret and surfaced to the API as Security__ApiKey; the API's analyse endpoints (/api/articles/{id}/analyze, /api/articles/analyze-missing) require this value in the X-Api-Key request header. Both apps' managed identities are granted Key Vault Secrets User.
Public URLs (default prefix newsagg):
| Service | URL |
|---|---|
| SPA (primary) | https://newsagg-ui.azurewebsites.net |
| API (direct) | https://newsagg-web.azurewebsites.net |
| Function host | https://newsagg-func.azurewebsites.net |
- Active subscription with permission to create resource groups, App Service, PostgreSQL, ACR, Key Vault
- Region: newzealandnorth (default in Bicep and workflows)
- Sufficient quota for Basic App Service plan (B1) and Burstable PostgreSQL (B1ms)
- Azure CLI —
az login - Azure Developer CLI —
azdfor local provision
- Repository on GitHub with
mainbranch - Admin access to Settings → Secrets and variables → Actions
- Environment newsagg-production (for infra workflow)
Use one service principal for infra + UI, and per-app federated credentials for web/func (as generated by Azure Deployment Center), or consolidate all workflows onto a single SP with Contributor on the resource group.
- Create an App registration (or use existing) in Microsoft Entra ID.
- Create a client secret only if not using federated credentials (workflows use OIDC, not secrets).
- Add Federated credential (GitHub Actions):
- Issuer:
https://token.actions.githubusercontent.com - Subject:
repo:<ORG>/<REPO>:environment:newsagg-production(for infra) - Additional credential for UI if repo-level:
repo:<ORG>/<REPO>:ref:refs/heads/main
- Issuer:
- Assign Contributor (or narrower: resource group scoped) on the target subscription or resource group.
Record:
- Application (client) ID →
AZURE_CLIENT_ID - Directory (tenant) ID →
AZURE_TENANT_ID - Subscription ID →
AZURE_SUBSCRIPTION_ID
For each of newsagg-web and newsagg-func (must exist after Phase 2 provision, or configure after):
- Azure Portal → App Service / Function App → Deployment Center → GitHub Actions.
- Connect repo; Azure creates federated identity and adds secrets with names like
AZUREAPPSERVICE_CLIENTID_*. - Ensure deployment targets
newsagg-webandnewsagg-func(not legacy*-rspb2enames).
Workflows already reference the generated secret names in:
.github/workflows/newsagg-web.yml.github/workflows/newsagg-func.yml
If you recreate apps, update those workflow secret names to match GitHub.
Used only by azure-deployment.yml.
| Type | Name | Required | Notes |
|---|---|---|---|
| Secret | AZURE_CLIENT_ID |
Yes | Infra/UI OIDC app |
| Secret | AZURE_TENANT_ID |
Yes | |
| Secret | AZURE_SUBSCRIPTION_ID |
Yes | |
| Secret | POSTGRES_ADMIN_PASSWORD |
Yes | Strong password; stored in Key Vault via Bicep |
| Secret | POSTGRES_ADMIN_USERNAME |
No | Default pgadmin |
| Secret | API_SECURITY_KEY |
Yes | Random key for the protected analyse endpoints; stored in Key Vault as ApiSecurityKey and sent by clients as X-Api-Key |
| Secret | Purpose |
|---|---|
AZUREAPPSERVICE_CLIENTID_66485046BD9349D28AE9764C8A0D0C02 |
Federated client for newsagg-web |
AZUREAPPSERVICE_TENANTID_7880779025AB4F18A1AB6BF80BBC7436 |
|
AZUREAPPSERVICE_SUBSCRIPTIONID_644B3FB5ACB9426CB84179043D136F2E |
| Secret | Purpose |
|---|---|
AZUREAPPSERVICE_CLIENTID_04B98E97454146D88FE18C8304E718B3 |
Federated client for newsagg-func |
AZUREAPPSERVICE_TENANTID_A1705BCD3CB645B5A9BCEF34AE8E3294 |
|
AZUREAPPSERVICE_SUBSCRIPTIONID_8102D7E9228544DA8D6C857538331779 |
Reuses infra trio (same SP as Phase 1a):
| Secret | Purpose |
|---|---|
AZURE_CLIENT_ID |
|
AZURE_TENANT_ID |
|
AZURE_SUBSCRIPTION_ID |
SP needs: AcrPush on newsaggacr, Website Contributor on newsagg-ui (or Contributor on resource group).
| Variable | Default | When to set |
|---|---|---|
AZD_PREFIX |
newsagg |
Custom resource prefix → myapp-web, myappacr, etc. |
AZURE_RESOURCE_GROUP |
rg-newsagg-prod |
If azd created a different RG name |
After first provision, confirm RG name:
az group list --query "[?contains(name,'newsagg')].name" -o tsv- Configure Phase 2 secrets in newsagg-production.
- Actions → Azure Deployment → Run workflow (manual dispatch).
- This runs
azd up: provisions Bicep and deploys backend + function via azd (bootstrap).
Subsequent pushes to infra/** or azure.yaml run azd provision only (no app redeploy from this workflow).
.\scripts\deploy-azure.ps1 `
-PostgresAdminPassword '<strong-password>' `
-ApiSecurityKey '<random-api-key>' `
-ProvisionOnly # infra only; omit for full azd upUses azd environment newsagg-prod (matches GitHub).
- Resource group (via azd)
- PostgreSQL 17, database
newsagg - Key Vault + secrets (Postgres connection string,
AzureLanguageKey,ApiSecurityKey, optionalHuggingFaceToken) - Linux App Service plan (B1)
newsagg-web,newsagg-func,newsagg-ui- ACR
newsaggacr - Storage account (Functions runtime)
Wait until provision succeeds before Phase 4.
| Workflow | Triggers on | Deploys |
|---|---|---|
newsagg-web.yml |
backend/**, scraper/**, newsagg.sln |
API zip → newsagg-web |
newsagg-func.yml |
functionapp/** |
Python package → newsagg-func |
newsagg-ui.yml |
After web or func workflow succeeds; or frontend/** push |
Docker image → ACR → newsagg-ui |
All share concurrency group azure-newsagg-<ref> so deploys do not overlap.
Run in order:
- Build and deploy ASP.Net Core app to Azure Web App - newsagg-web
- Build and deploy Python project to Azure Function App - newsagg-func
- Build and deploy UI to Azure Web App - newsagg-ui
newsagg-ui waits for https://newsagg-web.azurewebsites.net/api/health before deploying.
On workflow_dispatch, azure-deployment also runs azd deploy for:
backend→newsagg-webscraper→newsagg-funcfrontend→newsagg-ui(Docker via ACR;azure.yamlpredeploy hook waits for backend health)
GitHub newsagg-ui.yml is still used for UI-only CI after web/func workflows.
# Backend health
curl -s https://newsagg-web.azurewebsites.net/api/health
# Frontend (SPA)
curl -sI https://newsagg-ui.azurewebsites.net/
# Articles (after scraper has run)
curl -s https://newsagg-ui.azurewebsites.net/api/articles
# Manual scraper trigger (Function key may be required for direct func URL)
curl -X POST https://newsagg-web.azurewebsites.net/api/scraper/refresh
# Re-analysis endpoints require the API key (X-Api-Key = apiSecurityKey parameter)
curl -X POST "https://newsagg-web.azurewebsites.net/api/articles/analyze-missing?limit=50" \
-H "X-Api-Key: <api-security-key>"Note: the analyse endpoints and scraper/refresh are rate limited to 5 requests per minute per client IP (429 Too Many Requests when exceeded).
| Check | Expected |
|---|---|
| UI loads in browser | React app at newsagg-ui |
/api/health via UI |
200 from proxied backend |
| Function timer | Logs in newsagg-func → calls SCRAPER_REFRESH_URL |
| PostgreSQL | Articles table populated after refresh |
| Task | How |
|---|---|
| Infra change | Push infra/** → azure-deployment provisions |
| API change | Push backend/** → newsagg-web |
| Timer/scraper change | Push functionapp/** → newsagg-func |
| UI change | Push frontend/** → newsagg-ui (or auto after web/func) |
| Full reprovision + azd apps | Manual Azure Deployment workflow |
| Local full stack | docker-compose up or scripts/start-compose.ps1 |
- Standard environment:
newsagg-production(GitHub) +rg-newsagg-prod(Azure) - Region lock: keep all primary services in
newzealandnorth - API discoverability: Swagger available at
https://newsagg-web.azurewebsites.net/docs - Readiness endpoint:
GET /api/health/ready - Growth endpoints for attribution and conversion:
POST /api/growth/subscribePOST /api/growth/eventsGET /api/growth/summaryGET /api/growth/scale-gate
Operational runbook and rollback steps are maintained in Production-Runbook.md.
| Symptom | Likely cause | Fix |
|---|---|---|
| Web deploy 401/403 | OIDC not configured for newsagg-web |
Re-run Deployment Center or update AZUREAPPSERVICE_* secrets |
| Func deploy fails | Wrong Python package layout | Ensure host.json at zip root; check EnableWorkerIndexing in Bicep |
| UI 502 / container won't start | No image in ACR yet | Run newsagg-ui workflow after ACR exists |
UI /api errors |
Backend down or wrong BACKEND_URL |
Check newsagg-web health; app setting on newsagg-ui |
| azd / UI RG not found | Wrong AZURE_RESOURCE_GROUP |
Set GitHub variable to actual RG from az group list |
| Postgres connection fails | Key Vault access / firewall | Confirm web app managed identity has Key Vault Secrets User |
| Triple deploy on every commit | Old *-rspb2e workflows still in repo |
Delete legacy workflow files if present |
| Resource | Name |
|---|---|
| Resource group | rg-newsagg-prod (azd default; confirm after provision) |
| azd environment | newsagg-prod |
| Web app | newsagg-web |
| Function app | newsagg-func |
| UI app | newsagg-ui |
| ACR | newsaggacr |
| PostgreSQL server | newsaggpg |
| Key Vault | newsaggkv |
- Azure subscription ready
- App registration + federated credential for
newsagg-production - GitHub environment
newsagg-productionwith 4–5 secrets - Repository secrets for web + func (
AZUREAPPSERVICE_*) - Repository secrets for UI (
AZURE_CLIENT_*) or shared with infra - Optional:
AZURE_RESOURCE_GROUP,AZD_PREFIXvariables - Run Azure Deployment (workflow_dispatch) — provision + bootstrap apps
- Run or verify newsagg-web and newsagg-func workflows
- Run or verify newsagg-ui workflow
- Open https://newsagg-ui.azurewebsites.net and confirm articles load
- Remove orphaned Azure resources
newsagg-*-rspb2eif no longer used
- Deployment.md — Docker Compose and general deployment
- Quickstart.md — Local development
- APIDoc.md — REST API reference