Azure Infrastructure
Resource map
Section titled “Resource map”flowchart TB
subgraph ACA [Azure Container Apps]
CloudApp[Cloud Backend API]
WorkerApp[Cloud Worker]
MgmtApp[Management Backend]
IngestApp[Ingestor Service]
end
subgraph Storage [Data Layer]
SQL[(Azure SQL Server - Cloud DB + Management DB)]
Blob[Azure Blob Storage - update artifacts]
end
subgraph Secrets [Key Vaults]
CloudKV[kv-mz-cloud]
ManageKV[kv-mz-manage]
end
ACR[Azure Container Registry] -->|Docker pull| CloudApp
ACR -->|Docker pull| WorkerApp
ACR -->|Docker pull| MgmtApp
ACR -->|Docker pull| IngestApp
CloudKV -->|secrets| CloudApp
CloudKV -->|secrets| WorkerApp
CloudKV -->|secrets| IngestApp
ManageKV -->|secrets| MgmtApp
CloudApp --> SQL
WorkerApp --> SQL
MgmtApp --> SQL
IngestApp --> SQL
CloudApp -->|OTA artifacts| Blob
CloudApp -->|OTLP gRPC| OtelCollector[OTel Collector internal]
WorkerApp -->|OTLP gRPC| OtelCollector
MgmtApp -->|OTLP gRPC| OtelCollector
IngestApp -->|OTLP gRPC| OtelCollector
OtelCollector --> AppInsights[Application Insights x4]
Azure SQL Server
Section titled “Azure SQL Server”- Single Azure SQL Server instance with two databases: Cloud (
db-monozu-cloud-{env}) and Management (db-monozu-manage-{env}) - Multi-tenant isolation via Row-Level Security (RLS) — see Multi-Tenancy
- Connection: Managed Identity (production) or SQL auth (
DATABASE_URL) for local dev - Migrations: Flyway runs as a post-deploy step in Azure DevOps pipelines
Azure Container Apps
Section titled “Azure Container Apps”Each backend runs as a Linux container pulled from ACR in a shared Container Apps environment (cae-mz-{env}):
| App | Resource | Ingress |
|---|---|---|
| Cloud API | cloud-mz-{env} | External HTTPS |
| Worker | worker-mz-{env} | None (background jobs) |
| Ingestor | ingest-mz-{env} | External HTTPS |
| Management | manage-mz-{env} | External HTTPS |
| OTel Collector | otel-mz-{env} | Internal HTTPS (gRPC OTLP) |
| Setting | Value |
|---|---|
| Runtime | Docker (Linux) |
| Identity | User-assigned managed identity per app |
| Key Vault access | Key Vault Secrets User role on the service’s KV |
| Telemetry | OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL=grpc, OTEL_RESOURCE_ATTRIBUTES, SERVICE_NAME |
Secrets are loaded from Key Vault via Container App secret references.
Azure Container Registry
Section titled “Azure Container Registry”- Shared ACR instance (
acr-monozu) for all environments - Images tagged by git commit SHA:
acr-monozu.azurecr.io/cloud-backend:<sha> - Container Apps pull images tagged by pipeline (e.g.
acr-mz-dev.azurecr.io/cloud-backend:<sha>)
Azure Key Vault
Section titled “Azure Key Vault”Two separate Key Vaults — one per service — to limit blast radius:
| Vault | Contents |
|---|---|
kv-mz-cloud-{env} | SECRET_KEY, CLOUD_INTERNAL_API_TOKEN, CLOUD_WEBHOOK_HMAC_SECRET, MANAGEMENT_API_URL, MS_CLIENT_ID, MS_CLIENT_SECRET, SNAP_PUBLISH_TOKEN, devices-ca-cert, devices-ca-key, ingestor-tls-cert, ingestor-tls-key — transactional email uses ACS managed identity (COMMUNICATION_SERVICES_ENDPOINT / EMAIL_FROM_ADDR on ACA, not KV) |
kv-mz-manage-{env} | SECRET_KEY, CLOUD_INTERNAL_API_TOKEN, CLOUD_WEBHOOK_HMAC_SECRET, CLOUD_API_URL, MS_CLIENT_ID, MS_CLIENT_SECRET, MS_TENANT_ID |
kv-mz-shared-{env} | App Insights connection strings for the OTel collector (appinsights-connection-string-{cloud,worker,ingest,manage}) — seeded by seed-otel-collector-secrets.sh |
CLOUD_INTERNAL_API_TOKEN and CLOUD_WEBHOOK_HMAC_SECRET must have the same value in both vaults. See docs/cloud-management-env.md for the full secret list.
Secret sync flow (Container Apps)
Section titled “Secret sync flow (Container Apps)”Key Vault remains the source of truth for secret values. Infra deploy:
- Pre-deploy:
fetch-kv-secrets-for-bicep.shlists enabled secrets in each app vault and writes KV-ref parameters (keyVaultUrl+ UAMI identity) for Bicep — no gap after deploy. - Bicep deploy:
containerApp.bicepdeclareskvSecretRefs/kvSecretEnvalongsideplainEnv. - Post-deploy:
sync-all-kv-secrets-aca.shrefreshes KV refs after value rotation (ACA does not auto-refresh cached secret values).
On first deploy with an empty vault, step 1 writes empty arrays; run sync after seeding KV secrets.
OTel collector App Insights CS in kv-mz-shared-{env}: seed-otel-collector-secrets.sh (pipeline post-deploy) requires Key Vault Secrets Officer on the shared vault for the infra pipeline service principal. Bicep grants this automatically when infraPipelinePrincipalId is passed (see infrastructure.yml deploy step). One-time manual grant if seed fails before the next infra deploy:
az role assignment create --role "Key Vault Secrets Officer" \ --assignee-object-id "$(az ad sp show --id "$(az account show --query user.name -o tsv)" --query id -o tsv)" \ --scope "$(az keyvault show --name kv-mz-shared-dev --query id -o tsv)"The collector still receives connection strings from Bicep deploySecrets even when KV seed fails.
Observability (OTLP → Collector → Application Insights)
Section titled “Observability (OTLP → Collector → Application Insights)”Apps export OTLP/gRPC to the internal collector (otel-mz-{env}). The collector routes by service.name to four App Insights components, workspace-linked to Log Analytics. See Telemetry architecture.
| Signal | Path | Query table |
|---|---|---|
| Logs (WARN+) | App → OTLP → collector → App Insights | AppTraces |
| HTTP requests | App → OTLP → collector → App Insights | AppRequests (operation_kind=http) |
| Worker jobs | App → OTLP → collector → App Insights | AppRequests (operation_kind=job) |
| SQL / HTTP / Azure deps | OTel → collector → App Insights | AppDependencies |
| DB pool / queue depth | OTel metrics → collector → App Insights | AppMetrics |
| Container stdout (logfmt) | Not persisted | az containerapp logs show only |
SERVICE_NAME per app (→ AppRoleName in LAW): monozu-cloud-backend, monozu-cloud-worker, monozu-ingestor, monozu-management-backend.
Grafana dashboards (portal, $0)
Section titled “Grafana dashboards (portal, $0)”Bicep deploys two Azure Monitor dashboards with Grafana in rg-monozu-shared-{env}:
dash-mz-app-{env}— application & databasedash-mz-infra-{env}— Container Apps, SQL, jobs queue
Legacy dash-monozu-obs-{env} remains until manual cleanup.
See Observability dashboards and Telemetry architecture.
Application Insights components (appi-*-{env}) are workspace-linked to LAW for enrichment and querying.
Azure Communication Services (Email)
Section titled “Azure Communication Services (Email)”Transactional email for Cloud Backend and Worker (internal/services/email.go) — invites, vendor OTP, maintenance notifications:
| Resource | Naming | Notes |
|---|---|---|
| Communication Service | acs-mz-{env} | rg-monozu-shared-{env}; Contributor role for cloud + worker UAMIs |
| Email Service | email-mz-{env} | Parent for verified sender domain |
| Sender domain | cloud.monozu.io (prd) / cloud-dev.monozu.io (dev) | Customer-managed; DNS via Infrastructure/scripts/setup_acs_email_dns.py |
| From address | noreply@cloud.monozu.io / noreply@cloud-dev.monozu.io | EMAIL_FROM_ADDR on ACA (not a Key Vault secret) |
COMMUNICATION_SERVICES_ENDPOINT is set as a Container App plain env var from Bicep. Auth uses managed identity — no RESEND_API_KEY in Key Vault for the API.
Marketing contact form email remains on Resend via the Cloudflare Worker — see Cloudflare.
Managed Identity
Section titled “Managed Identity”Production Container Apps use user-assigned managed identity for:
- Azure Key Vault access (no credentials in app settings)
- Azure SQL connection (no password — uses
azidentity.ManagedIdentityCredential) - Azure Blob Storage (OTA / update artifacts)
Grant each app’s managed identity the following roles:
- Key Vault Secrets User on its Key Vault
- SQL db_datareader / db_datawriter on its database (via
CREATE USER ... FROM EXTERNAL PROVIDERin SQL) - Storage Blob Data Reader on the update-artifacts container (Cloud Backend only)