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/:
-
build_and_push_imagesjob — builds both service images from the repo root (so the Dockerfiles canCOPY sdk/alongside the app folder) and pushes two tags to Artifactory:$(Build.BuildId)(unique, traceable per run) andlatest. -
deploy_servicesjob — runs only onmainmerges (not PRs), using thesemantic-eval-devAzure DevOps service connection. Updates each Container App's active image to the$(Build.BuildId)tag just pushed, then re-pinstargetPortviaaz containerapp ingress updateto 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.