Container Apps Environment — Setup Reference
Last verified: 2026-03-16
This document describes the current Azure Container Apps (CAE) setup used to run the Audissey worker across all environments. It serves as the single source of truth for infrastructure references, networking, identity, DNS, and environment variables.
Architecture Overview
All worker instances run on a single shared CAE in North Europe:
| Resource | Value |
|---|---|
| CAE name | audissey-cae-northeu |
| Resource Group | audissey-staging-rg |
| Region | North Europe |
| Plan | Consumption (serverless) |
| Internal (VNet-only) | Yes (publicNetworkAccess=Disabled) |
| Default domain | salmonglacier-eb702166.northeurope.azurecontainerapps.io |
| Static IP (ILB) | 10.252.1.150 |
| Log Analytics | audissey-law-workers-ne (customerId: 719e6bd2-1533-4e63-923f-627218382a7a) |
Why North Europe?
West Europe AKS free-tier capacity is exhausted — Azure returns AKSCapacityHeavyUsage when trying to create new CAEs there. The migration to NE was performed on 2026-03-09. Prod should eventually move to a dedicated WE CAE when capacity frees up (see Future: Prod Isolation).
Container Apps per Environment
Dev
| Field | Value |
|---|---|
| Container App | audissey-worker-dev |
| Resource Group | audissey-dev-rg |
| FQDN | audissey-worker-dev.salmonglacier-eb702166.northeurope.azurecontainerapps.io |
| Image | audissey.azurecr.io/audissey-worker:<tag> |
| Replicas | min: 0, max: 2 (KEDA scale-to-zero) |
| Managed Identity (system) | 59ab773d-7c0f-4499-b4d0-1068b87231df |
| RBAC | AcrPull on audissey.azurecr.io |
| API calling it | audissey-api-dev (VNet: audissey-vnet-workers-we/appsvc-integration) |
Environment variables:
NODE_ENV, LOG_LEVEL, COSMOS_DB_NAME, MONGODB_DB_NAME, MONGO_URI,
REDIS_CONNECTION_STRING, WORKER_API_KEY, API_URL, ML_SERVICE_URL,
APPLICATIONINSIGHTS_CONNECTION_STRING, PODCAST_INDEX_API_KEY,
PODCAST_INDEX_API_SECRET
Staging
| Field | Value |
|---|---|
| Container App | audissey-worker-staging |
| Resource Group | audissey-staging-rg |
| FQDN | audissey-worker-staging.salmonglacier-eb702166.northeurope.azurecontainerapps.io |
| Image | audissey.azurecr.io/audissey-worker:<tag> |
| Replicas | min: 0, max: 2 (KEDA scale-to-zero) |
| Managed Identity (system) | 27ff0af7-806b-4146-8c1f-97c6b110b7ce |
| RBAC | AcrPull on audissey.azurecr.io + Key Vault Secrets User on audissey-kv-staging |
| API calling it | audissey-api-staging (VNet: audissey-vnet-staging/appsvc-integration) |
Environment variables:
NODE_ENV, LOG_LEVEL, COSMOS_DB_NAME, MONGODB_DB_NAME, MONGO_URI,
REDIS_CONNECTION_STRING, WORKER_API_KEY, API_URL, ML_SERVICE_URL,
APPLICATIONINSIGHTS_CONNECTION_STRING
Production
| Field | Value |
|---|---|
| Container App | audissey-worker |
| Resource Group | audissey-prod-rg |
| FQDN | audissey-worker.salmonglacier-eb702166.northeurope.azurecontainerapps.io |
| Image | audissey.azurecr.io/audissey-worker:<tag> |
| Replicas | min: 0, max: 3 (KEDA scale-to-zero) |
| Managed Identity (system) | 172dcef0-1c87-4524-ac0a-e05702d8f3d9 |
| RBAC | AcrPull on audissey.azurecr.io + Key Vault Secrets User on audissey-kv-production |
| API calling it | audissey-api (VNet: audissey-vnet-prod/appsvc-integration) |
Environment variables:
NODE_ENV, LOG_LEVEL, COSMOS_DB_NAME, MONGODB_DB_NAME, REDIS_CONNECTION_STRING,
MONGO_URI, REDIS_HOST, REDIS_PORT, REDIS_TLS, REDIS_PASSWORD,
WORKER_API_KEY, API_URL, ML_SERVICE_URL, APPLICATIONINSIGHTS_CONNECTION_STRING,
PODCAST_INDEX_API_KEY, PODCAST_INDEX_API_SECRET
Note: Prod has extra
REDIS_HOST/PORT/TLS/PASSWORDenv vars in addition toREDIS_CONNECTION_STRING. These are legacy — consolidate toREDIS_CONNECTION_STRINGonly when convenient.
Networking
VNets
| VNet | Address Space | Region | Resource Group | Purpose |
|---|---|---|---|---|
audissey-vnet-workers-ne |
10.252.0.0/16 |
North Europe | audissey-staging-rg |
CAE infrastructure |
audissey-vnet-workers-we |
10.251.0.0/16 |
West Europe | audissey-staging-rg |
Private Endpoints (Redis), dev API integration |
audissey-vnet-dev |
10.10.0.0/16 |
West Europe | audissey-dev-rg |
Dev App Services |
audissey-vnet-staging |
10.20.0.0/16 |
West Europe | audissey-staging-rg |
Staging App Services |
audissey-vnet-prod |
10.30.0.0/16 |
West Europe | audissey-prod-rg |
Prod App Services + future prod CAE |
Subnets — `audissey-vnet-workers-ne`
| Subnet | Prefix | Delegation | Usage |
|---|---|---|---|
containerapps-infra |
10.252.0.0/23 |
Microsoft.App/environments |
CAE audissey-cae-northeu |
Subnets — `audissey-vnet-workers-we` (still active)
| Subnet | Prefix | Delegation | Usage |
|---|---|---|---|
containerapps-infra |
10.251.0.0/23 |
Microsoft.App/environments |
Unused (old CAE deleted) |
private-endpoints |
10.251.2.0/24 |
— | Redis PE audissey-redis-pe-workers (10.251.0.6) |
appsvc-integration |
10.251.3.0/24 |
Microsoft.Web/serverFarms |
Dev API VNet integration |
Subnets — `audissey-vnet-prod` (future prod CAE ready)
| Subnet | Prefix | Delegation | Usage |
|---|---|---|---|
appsvc-integration |
10.30.1.0/24 |
Microsoft.Web/serverFarms |
Prod API VNet integration |
private-endpoints |
10.30.2.0/24 |
— | Key Vault PE |
containerapps-infra |
10.30.4.0/23 |
Microsoft.App/environments |
Pre-provisioned for future prod CAE |
VNet Peerings (from `audissey-vnet-workers-ne`)
All bidirectional, status: Connected.
| Peering Name | Remote VNet |
|---|---|
ne-workers-to-we-workers |
audissey-vnet-workers-we |
ne-workers-to-staging |
audissey-vnet-staging |
ne-workers-to-prod |
audissey-vnet-prod |
ne-workers-to-dev |
audissey-vnet-dev |
Legacy peerings (WE workers VNet → env VNets) also remain active.
API → Worker connectivity path
| API | Integrated VNet/Subnet | Route to NE CAE |
|---|---|---|
audissey-api-dev |
audissey-vnet-workers-we/appsvc-integration |
Peering: WE-workers ↔ NE-workers (same VNet family) |
audissey-api-staging |
audissey-vnet-staging/appsvc-integration |
Peering: staging ↔ NE-workers |
audissey-api (prod) |
audissey-vnet-prod/appsvc-integration |
Peering: prod ↔ NE-workers |
DNS
CAE Private DNS Zone
| Field | Value |
|---|---|
| Zone | salmonglacier-eb702166.northeurope.azurecontainerapps.io |
| Resource Group | audissey-staging-rg |
| Record | * (wildcard A) → 10.252.1.150 |
VNet links:
| Link Name | VNet |
|---|---|
link-workers-ne |
audissey-vnet-workers-ne |
link-workers-we |
audissey-vnet-workers-we |
link-staging |
audissey-vnet-staging |
link-prod |
audissey-vnet-prod |
link-dev |
audissey-vnet-dev |
All 5 VNets can resolve
*.salmonglacier-eb702166.northeurope.azurecontainerapps.io→10.252.1.150.
Private Endpoint DNS Zones (in `audissey-staging-rg`, linked to NE VNet)
These enable the NE workers to reach Cosmos, Redis, and Key Vault via Private Endpoints:
| Zone | Link for NE VNet |
|---|---|
privatelink.mongo.cosmos.azure.com |
link-ne-workers-cosmos |
privatelink.redis.cache.windows.net |
link-ne-workers-redis |
privatelink.vaultcore.azure.net |
link-ne-workers-kv |
API App Settings Reference
The API App Services must have WORKER_URL and WORKER_API_URL pointing to the correct worker FQDN. These are managed in:
deploy/config/appsettings/api/dev.jsondeploy/config/appsettings/api/staging.jsondeploy/config/appsettings/api/prod.json
Current values:
| Environment | WORKER_URL / WORKER_API_URL |
|---|---|
| Dev | https://audissey-worker-dev.salmonglacier-eb702166.northeurope.azurecontainerapps.io/api |
| Staging | https://audissey-worker-staging.salmonglacier-eb702166.northeurope.azurecontainerapps.io/api |
| Prod | https://audissey-worker.salmonglacier-eb702166.northeurope.azurecontainerapps.io/api |
These are applied to App Services via the ensure-appsettings.js script in the release pipeline.
Deployment Pipeline
The worker release pipeline (azure-pipelines-worker-release.yml) deploys via:
az containerapp update --name <APP_NAME> --resource-group <RG> --image <IMAGE>
It does not reference the CAE name — only the container app name and its RG. This makes it migration-safe.
| Stage | Container App | Resource Group |
|---|---|---|
| Dev | audissey-worker-dev |
audissey-dev-rg |
| Staging | audissey-worker-staging |
audissey-staging-rg |
| Prod | audissey-worker |
audissey-prod-rg |
Cross-RG RBAC for Shared CAE
Because the CAE lives in audissey-staging-rg but dev/prod Container Apps are in their own RGs, the dev and prod service principals need Contributor scoped to the CAE resource. Without this, az containerapp secret set and az containerapp update fail with LinkedAuthorizationFailed (missing Microsoft.App/managedEnvironments/join/action).
| Service Principal | Object ID | Home RG | CAE RBAC |
|---|---|---|---|
Dev SP (azure-dev-connection) |
95fabf15-2d2f-4ae7-8d4f-0a41e5fee3cf |
audissey-dev-rg |
Contributor on CAE |
Prod SP (azure-prod-connection) |
27fa4864-df77-47e6-839f-e4d8a60feb37 |
audissey-prod-rg |
Contributor on CAE |
Staging SP (azure-staging-connection) |
e1952205-5824-422e-b08f-3746c6398203 |
audissey-staging-rg |
Already Contributor on entire RG |
These are documented in deploy/config/rbac/staging.json under resourceScopedRoles.
To re-apply if needed:
CAE_SCOPE="/subscriptions/7141477e-818d-4a88-be77-1ba6c04d42a0/resourceGroups/audissey-staging-rg/providers/Microsoft.App/managedEnvironments/audissey-cae-northeu"
# Dev SP
az role assignment create --assignee "95fabf15-2d2f-4ae7-8d4f-0a41e5fee3cf" --role "Contributor" --scope "$CAE_SCOPE"
# Prod SP
az role assignment create --assignee "27fa4864-df77-47e6-839f-e4d8a60feb37" --role "Contributor" --scope "$CAE_SCOPE"
Container Registry
| Field | Value |
|---|---|
| Registry | audissey.azurecr.io |
| Resource Group | audissey-shared-rg |
| Image | audissey-worker |
| Pull method | Managed Identity (acrUseManagedIdentityCreds: true) |
All 3 worker identities have AcrPull role on the registry.
Log Analytics
| Workspace | Region | Resource Group | customerId | Used by |
|---|---|---|---|---|
audissey-law-workers-ne |
North Europe | audissey-staging-rg |
719e6bd2-1533-4e63-923f-627218382a7a |
audissey-cae-northeu (active) |
audissey-law-workers-prod |
West Europe | audissey-prod-rg |
52a2573a-6a80-4373-aa85-96a1c12ea80f |
Reserved for future WE prod CAE |
To query worker logs:
# Dev worker logs (last 1h)
az monitor log-analytics query \
-w 719e6bd2-1533-4e63-923f-627218382a7a \
--analytics-query "ContainerAppConsoleLogs_CL | where ContainerAppName_s == 'audissey-worker-dev' | where TimeGenerated > ago(1h) | order by TimeGenerated desc | take 50" \
-o table
# Replace ContainerAppName_s with 'audissey-worker-staging' or 'audissey-worker' for other envs.
Future: Prod Isolation (WE CAE)
When West Europe AKS capacity frees up, prod should be moved to a dedicated CAE for full environment isolation:
Pre-provisioned resources (ready to use):
- Subnet:
audissey-vnet-prod/containerapps-infra(10.30.4.0/23, delegated toMicrosoft.App/environments) - Log Analytics:
audissey-law-workers-prod(West Europe,audissey-prod-rg)
Steps to migrate:
- Create CAE in WE:
az containerapp env create \ --name audissey-cae-prod \ --resource-group audissey-prod-rg \ --location westeurope \ --infrastructure-subnet-resource-id "/subscriptions/7141477e-818d-4a88-be77-1ba6c04d42a0/resourceGroups/audissey-prod-rg/providers/Microsoft.Network/virtualNetworks/audissey-vnet-prod/subnets/containerapps-infra" \ --internal-only true \ --logs-workspace-id 52a2573a-6a80-4373-aa85-96a1c12ea80f \ --logs-workspace-key <KEY> - Note the new CAE domain and static IP.
- Create a new private DNS zone for the prod CAE domain.
- Add wildcard A record pointing to the static IP.
- Link the DNS zone to
audissey-vnet-prod(and any other VNets that need to resolve it). - Recreate
audissey-workeron the new prod CAE with the same env vars and identity. - Re-assign RBAC (
AcrPull+Key Vault Secrets User) to the new identity. - Update
WORKER_URL/WORKER_API_URLindeploy/config/appsettings/api/prod.json. - Apply settings to
audissey-apiApp Service. - Delete the old
audissey-workerfromaudissey-cae-northeu.
How to check if WE capacity is available:
az containerapp env create \
--name test-cae-we-capacity \
--resource-group audissey-prod-rg \
--location westeurope \
--internal-only true \
--logs-destination none
# If it succeeds, delete it and proceed with the real creation.
# If AKSCapacityHeavyUsage → still blocked.
KEDA Scale-to-Zero Configuration
Last updated: 2026-03-17
All worker Container Apps use KEDA Redis List scalers to scale from 0 to N replicas when BullMQ queues have pending jobs.
Scale Settings
| Environment | minReplicas | maxReplicas | pollingInterval | cooldownPeriod | Total rules |
|---|---|---|---|---|---|
| Dev | 0 | 2 | 30s | 300s | 24 |
| Staging | 0 | 2 | 30s | 300s | 24 |
| Prod | 0 | 3 | 30s | 300s | 24 |
Dual-Rule Pattern (`:wait` + `:active`)
Each BullMQ queue gets two KEDA scale rules:
:waitrule — triggers scale-up when new jobs arrive in the wait list:activerule — keeps the replica alive while a job is being processed
Without the :active rule, KEDA sees an empty :wait list during processing and scales to 0, killing the container mid-job. This was observed with snapshot-ml-training which runs for >2 hours on Cosmos DB (validated on dev: 2h21min continuous processing survived).
Monitored Queues
| BullMQ Queue | :wait Rule |
:active Rule |
|---|---|---|
run-schedules |
keda-redis-run-schedules |
keda-redis-run-schedules-active |
stream-health-check |
keda-redis-stream-health-check |
keda-redis-stream-health-check-active |
sync-radios |
keda-redis-sync-radios |
keda-redis-sync-radios-active |
sync-programs |
keda-redis-sync-programs |
keda-redis-sync-programs-active |
sync-podcasts |
keda-redis-sync-podcasts |
keda-redis-sync-podcasts-active |
sync-episodes |
keda-redis-sync-episodes |
keda-redis-sync-episodes-active |
enrich-radios |
keda-redis-enrich-radios |
keda-redis-enrich-radios-active |
scraping-queue |
keda-redis-scraping-queue |
keda-redis-scraping-queue-active |
content-freshness |
keda-redis-content-freshness |
keda-redis-content-freshness-active |
aggregate-content-popularity |
keda-redis-aggregate-content-popularity |
keda-redis-aggregate-content-popularity-active |
aggregate-user-features |
keda-redis-aggregate-user-features |
keda-redis-aggregate-user-features-active |
snapshot-ml-training |
keda-redis-snapshot-ml-training |
keda-redis-snapshot-ml-training-active |
Queues WITHOUT KEDA Scalers (by design)
| Queue | Reason |
|---|---|
user-data-deletion |
Event-driven (DELETE /users/me) — fires rarely; worker is usually up for other jobs |
feed-health-score |
Intentionally disabled (not in CronConfig defaults) |
backfill-episode-features |
Manual API endpoint only |
backfill-radios-dedupe-fields |
Manual API endpoint only |
sync-myradioonline |
Low-frequency manual trigger |
Source of Truth
The scripts/ensure-worker-keda.sh script is the pipeline source of truth for KEDA configuration. It reads the QUEUES array and generates the scale rules YAML that is applied during each worker release.
CRITICAL: The KEDA rule names and listName values must match the BullMQ @Processor('name') decorator in the worker code. A mismatch means KEDA watches a non-existent Redis key and the worker never scales up for that queue.
Verification
# Show KEDA rules for a worker
az containerapp show -g <RG> -n <APP> \
--query 'properties.template.scale.rules[].{name:name, listName:custom.metadata.listName}' \
-o table
Quick Reference Commands
# List all workers
az containerapp list \
--query "[].{name:name, rg:resourceGroup, fqdn:properties.configuration.ingress.fqdn, state:properties.provisioningState}" \
-o table
# Show worker logs (replace NAME and WORKSPACE_ID)
az containerapp logs show -n <NAME> -g <RG> --type console --tail 100
# Check worker identity RBAC
az role assignment list --assignee <PRINCIPAL_ID> --all \
--query "[].{role:roleDefinitionName, scope:scope}" -o table
# Verify API app setting
az webapp config appsettings list -n <API_APP> -g <RG> \
--query "[?name=='WORKER_URL'].value" -o tsv
# Check DNS resolution (from a VM in the VNet, or via nslookup)
nslookup audissey-worker-dev.salmonglacier-eb702166.northeurope.azurecontainerapps.io 168.63.129.16