Skip to content

Azure Infrastructure

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]
  • 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

Each backend runs as a Linux container pulled from ACR in a shared Container Apps environment (cae-mz-{env}):

AppResourceIngress
Cloud APIcloud-mz-{env}External HTTPS
Workerworker-mz-{env}None (background jobs)
Ingestoringest-mz-{env}External HTTPS
Managementmanage-mz-{env}External HTTPS
OTel Collectorotel-mz-{env}Internal HTTPS (gRPC OTLP)
SettingValue
RuntimeDocker (Linux)
IdentityUser-assigned managed identity per app
Key Vault accessKey Vault Secrets User role on the service’s KV
TelemetryOTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL=grpc, OTEL_RESOURCE_ATTRIBUTES, SERVICE_NAME

Secrets are loaded from Key Vault via Container App secret references.

  • 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>)

Two separate Key Vaults — one per service — to limit blast radius:

VaultContents
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.

Key Vault remains the source of truth for secret values. Infra deploy:

  1. Pre-deploy: fetch-kv-secrets-for-bicep.sh lists enabled secrets in each app vault and writes KV-ref parameters (keyVaultUrl + UAMI identity) for Bicep — no gap after deploy.
  2. Bicep deploy: containerApp.bicep declares kvSecretRefs / kvSecretEnv alongside plainEnv.
  3. Post-deploy: sync-all-kv-secrets-aca.sh refreshes 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:

Terminal window
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.

SignalPathQuery table
Logs (WARN+)App → OTLP → collector → App InsightsAppTraces
HTTP requestsApp → OTLP → collector → App InsightsAppRequests (operation_kind=http)
Worker jobsApp → OTLP → collector → App InsightsAppRequests (operation_kind=job)
SQL / HTTP / Azure depsOTel → collector → App InsightsAppDependencies
DB pool / queue depthOTel metrics → collector → App InsightsAppMetrics
Container stdout (logfmt)Not persistedaz containerapp logs show only

SERVICE_NAME per app (→ AppRoleName in LAW): monozu-cloud-backend, monozu-cloud-worker, monozu-ingestor, monozu-management-backend.

Bicep deploys two Azure Monitor dashboards with Grafana in rg-monozu-shared-{env}:

  • dash-mz-app-{env} — application & database
  • dash-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.

Transactional email for Cloud Backend and Worker (internal/services/email.go) — invites, vendor OTP, maintenance notifications:

ResourceNamingNotes
Communication Serviceacs-mz-{env}rg-monozu-shared-{env}; Contributor role for cloud + worker UAMIs
Email Serviceemail-mz-{env}Parent for verified sender domain
Sender domaincloud.monozu.io (prd) / cloud-dev.monozu.io (dev)Customer-managed; DNS via Infrastructure/scripts/setup_acs_email_dns.py
From addressnoreply@cloud.monozu.io / noreply@cloud-dev.monozu.ioEMAIL_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.

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 PROVIDER in SQL)
  • Storage Blob Data Reader on the update-artifacts container (Cloud Backend only)