Skip to content

Deployment

review-sync-service runs as an Azure Container App in Semantic-Evaluation-Dev-RG, in the semantic-eval-nonprod-env Workload Profiles environment in Sweden Central. This page covers the infrastructure layout, CI/CD pipeline, secret management, and routine operational tasks.

Infrastructure overview

graph TD
  subgraph VNet["AZUSC-SemanticEvaluationSystem nonprod vnet"]
    subgraph Subnet["Container-app-subnet 10.9.75.128/27"]
      subgraph Env["semantic-eval-nonprod-env Workload Profiles"]
        SS["scoring-service"]
        RS["review-sync-service"]
      end
    end
  end
  subgraph Azure["Azure platform services"]
    KV["semantic-eval-dev-kv Key Vault"]
    AF["semevalnpstorage Azure Files share service-state"]
    AR["artifactory.ramboll.com container registry"]
  end
  SS -->|"mounts /mnt/state"| AF
  RS -->|"mounts /mnt/state"| AF
  SS -->|"reads caller keys via managed identity"| KV
  RS -->|"reads caller keys via managed identity"| KV
  AR -->|"image pulled at deploy"| SS
  AR -->|"image pulled at deploy"| RS
  style VNet fill:#e9ebec,stroke:#273943,stroke-width:2px
  style Subnet fill:#dde6ec,stroke:#273943,stroke-width:2px
  style Env fill:#e5f4fd,stroke:#0098eb,stroke-width:2px
  style Azure fill:#f5f0e8,stroke:#cc8844,stroke-width:2px

The environment runs in a Workload Profiles configuration because the assigned Container-app-subnet is a /27 (32 addresses), which is below the /23 minimum required for a Consumption-only environment but meets the /27 Workload Profiles minimum.

Both services share one Azure Files volume (service-state) mounted at /mnt/state. The state files each service writes are disjoint — scoring-service writes connection_profiles.json and custom_rubric_requests/; review-sync-service writes bindings.json, langfuse_profiles.json, jobs/, checkpoints/, and cleanup_audits/.

Review-sync-service Container App properties

Property Value
Container App name review-sync-service
Environment semantic-eval-nonprod-env
Resource group Semantic-Evaluation-Dev-RG
Location Sweden Central
FQDN review-sync-service.bravecliff-5469885d.swedencentral.azurecontainerapps.io (VPN required; the .internal.-prefixed variant does not resolve correctly from outside the VNet)
Port 8002
Image artifactory.ramboll.com/semantic-evaluation/review-sync-service
Min replicas 1
Max replicas 2
Timezone UTC
State volume Azure Files share service-state, mounted at /mnt/state
Managed identity System-assigned — principal f5144038-688d-46a9-9585-37846fc04dc6
Caller keys review-sync-caller-<caller-id> secrets in semantic-eval-dev-kv, read by the service itself (see Caller keys)

The FQDN is assigned by Azure when the environment is created and will change if the environment is recreated. To retrieve the current value:

az containerapp show \
  --name review-sync-service \
  --resource-group Semantic-Evaluation-Dev-RG \
  --query "properties.configuration.ingress.fqdn" -o tsv

Environment variables

The Container App injects these environment variables into the container at startup:

Variable Value in Container App
REVIEW_SYNC_HOST 0.0.0.0
REVIEW_SYNC_PORT 8002
REVIEW_SYNC_RELOAD false
REVIEW_SYNC_STATE_DIR /mnt/state
CALLER_KEY_VAULT_URL https://semantic-eval-dev-kv.vault.azure.net/

CALLER_KEY_VAULT_URL names the vault the service reads its caller keys from. The Container App carries no API-key secret reference: the service lists the review-sync-caller- secrets itself with its managed identity. At startup it logs Loaded N callers: <ids>; with the variable unset it logs a warning and leaves every route open.

CI/CD pipeline

The pipeline at .devops/backend-services-azure-pipeline.yml runs on every main merge that touches scoring_app/, review_sync_app/, sdk/ or service_auth/:

  1. build_and_push_images job — builds both service images from the repo root (so the Dockerfiles can COPY sdk/ alongside the app folder) and pushes two tags to Artifactory: $(Build.BuildId) (unique, traceable per run) and latest.

  2. deploy_services job — runs only on main merges (not PRs), using the semantic-eval-dev Azure DevOps service connection. Updates each Container App's active image to the $(Build.BuildId) tag just pushed, then re-pins targetPort via az containerapp ingress update to ensure it survives the image update.

To trigger a forced redeploy without a code change, rerun the pipeline manually in Azure DevOps — the $(Build.BuildId) tag of that run will be deployed.

Secret management

Caller keys are stored in semantic-eval-dev-kv as Key Vault secrets. The service reads them itself with its managed identity, through the private endpoint.

KV secret Used by
review-sync-caller-<caller-id> review-sync-service: one caller key per caller
scoring-caller-<caller-id> scoring-service: one caller key per caller

Both managed identities hold Key Vault Secrets User RBAC on semantic-eval-dev-kv. Key Vault has public network access disabled; VPN is required to access the data plane from a developer machine.

Caller keys

Every caller (a pod, the workshop, the platform team) has its own key per service, so a key can be rotated or revoked without touching anyone else. A caller key is a Key Vault secret:

Secret field Meaning
Name review-sync-caller-<caller-id> (the scoring service uses scoring-caller-<caller-id>)
Value The caller's key
Tag role admin (the platform-admin caller) or pod; no tag means pod

Disabled and expired secrets are ignored. The service reloads the secrets every 5 minutes, so issuing, rotating or revoking a caller needs no deploy and takes effect within that interval. The callers that exist (platform-admin, workshop, checkmate, hr-mvp, shared) are listed in the scoring service's deployment page. The review-sync service has no admin-only route, so any of them may register Langfuse profiles.

Issuing a caller

The platform team issues keys, from a machine on VPN:

az keyvault secret set \
  --vault-name semantic-eval-dev-kv \
  --name review-sync-caller-<caller-id> \
  --value "$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')" \
  --tags role=pod -o none

Read the key to hand it over:

az keyvault secret show --vault-name semantic-eval-dev-kv \
  --name review-sync-caller-<caller-id> --query value -o tsv

Hand the value over out-of-band (1:1, never a shared channel) and never commit it. The caller sets it as REVIEW_SYNC_API_KEY in the pod's environment; the SDK client sends it as X-API-Key. Someone without Key Vault Secrets User on the vault asks the platform team; access and credentials names the contact.

Rotating a caller key

Run az keyvault secret set again with the same --name, a new --value and the same --tags role=<role>, and give the caller the new value. Tags belong to a secret version, so a new version without --tags turns an admin caller into a pod caller. The old value stops working at the next reload, at most 5 minutes later.

Revoking a caller

az keyvault secret set-attributes --vault-name semantic-eval-dev-kv \
  --name <secret> --enabled false

Deleting the secret has the same effect.

Operational checks

Check running status

az containerapp show \
  --name review-sync-service \
  --resource-group Semantic-Evaluation-Dev-RG \
  --query "{status:properties.runningStatus, revision:properties.latestRevisionName}"

Tail recent logs

az containerapp logs show \
  --name review-sync-service \
  --resource-group Semantic-Evaluation-Dev-RG \
  --tail 50

Verify the active image tag

az containerapp revision show \
  --name review-sync-service \
  --resource-group Semantic-Evaluation-Dev-RG \
  --revision "$(az containerapp show \
    --name review-sync-service \
    --resource-group Semantic-Evaluation-Dev-RG \
    --query properties.latestRevisionName -o tsv)" \
  --query "properties.template.containers[0].image" -o tsv

Test the health endpoint (from inside the VNet)

curl -s "https://review-sync-service.bravecliff-5469885d.swedencentral.azurecontainerapps.io/health"
# {"status": "ok"}

Reprovisioning from scratch

If the Container Apps environment needs to be recreated, the provisioning steps from Wave 7 apply. Prerequisites: semevalnpstorage storage account and service-state Azure Files share must exist; semantic-eval-dev-kv secrets must exist; the Container Apps extension and the semantic-eval-dev service connection must be active in Azure DevOps.

See docs/history/2026-07-22-wave-7-container-apps-deployment.md and docs/history/2026-07-22-wave-8-managed-identity-keyvault.md (at repository root) for the original step-by-step provisioning commands.