Skip to main content

πŸš€ Deployment Guide

This document describes how the CONDOR Calfire Inference system is deployed end-to-end on Azure using a single az deployment sub create command plus one manual approval step.

Architecture Overview​

CONDOR Infrastructure

The deployment is a single Bicep orchestration that provisions all resources, builds the server Docker image in ACR, bootstraps the GPU VM with Docker Compose, and wires the dashboard Container App to Front Door β€” in one shot.

Prerequisites​

RequirementDetails
Azure CLIaz v2.60+ with Bicep CLI
Azure loginaz login then az account set -s <subscription-id>
Subscription quotaGPU VM size (default Standard_NC48ads_A100_v4) in target region
Azure DevOps PATRead-scope PAT for repo clone (used by VM bootstrap and ACR build task)

Step 1 β€” Configure Parameters​

cd azure/infrastructure
cp parameters.example.bicepparam parameters.bicepparam

Edit parameters.bicepparam and replace every REPLACE_WITH_* placeholder with real values.

Security: parameters.bicepparam is git-ignored. Never commit this file.

Secrets (must be set)​

These parameters contain credentials and are marked @secure() in main.bicep:

ParameterPurpose
vmUsernameVM admin username (also stored in Key Vault as vm-username)
vmPasswordVM admin password (also stored in Key Vault as vm-password)
bootstrapRepoPatAzure DevOps PAT for git clone/fetch on VM and ACR build task
POSTGRES_USERPostgreSQL username (shared by DB, server, and client)
POSTGRES_PASSWORDPostgreSQL password (shared by DB, server, and client)
REDIS_PASSWORDRedis password
SERVER_SESSION_USERNAMEDashboard login username
SERVER_SESSION_PASSWORDDashboard login password
SERVER_SESSION_API_KEYSJSON map of API key β†’ service-user name
SERVER_SESSION_CSRF_SECRETHMAC signing secret for CSRF tokens
CLIENT_ALERTCA_PRD_BASEAlert California API base URL
CLIENT_FIRECAMS_BASEFireCams data API base URL
CLIENT_FIRECAMS_PANORAMA_BASEFireCams panorama API base URL
CLIENT_FIRECAMS_API_KEYFireCams API key
CLIENT_FIRECAMS_PANORAMA_API_KEYFireCams panorama API key
CLIENT_FIRECAMS_CAMERASComma-separated FireCams camera IDs
CLIENT_ALERTCA_CAMERASComma-separated Alert California camera IDs
CLIENT_CAMERASComma-separated default camera IDs

Deployment Configuration​

ParameterDefaultPurpose
ownerTagβ€”Tag value for resource ownership
locationswedencentralAzure region for all resources
resourceNamePrefixai4gl-condorPrefix used to derive all resource names
vmSizeStandard_NC48ads_A100_v4GPU VM SKU
dataDiskSizeGB4096Data disk size
bootstrapRepoBranchmainBranch checked out on the VM
bootstrapTimestamp1Increment to force VM re-bootstrap

Runtime Environment Variables​

The remaining parameters configure the server/.env, client/.env, db/.env, and redis/.env files written on the VM, plus the Container App environment. Their defaults in parameters.example.bicepparam are sensible starting points β€” review and adjust as needed.

Key patterns:

  • SERVER_* parameters map to server/.env (prefix stripped β†’ e.g. SERVER_POSTGRES_HOST becomes POSTGRES_HOST)
  • CLIENT_* parameters map to client/.env (prefix stripped)
  • POSTGRES_* (without SERVER_/CLIENT_ prefix) and REDIS_* are shared base values used by db/.env and redis/.env

Step 2 β€” Deploy​

A single command provisions everything:

az deployment sub create \
-l swedencentral \
-f main.bicep \
-p @parameters.bicepparam

Replace swedencentral with the location value in your parameters file.

What happens during deployment​

The Bicep orchestration (main.bicep at subscription scope) executes these modules in dependency order:

PhaseModuleWhat it does
1Resource GroupCreates <prefix>-rg
2NetworkVNet, 3 subnets (VM, private endpoints, Container App Env), NSGs, Bastion, public IPs
3StorageStorage Account for blob output
4Key VaultKey Vault with bastion, Postgres, Redis, and session secrets
5ACRContainer Registry with geo-replication
6Managed IdentityUser Assigned Managed Identity (UAMI)
7RBACAssigns Key Vault Secrets User, ACR Pull/Push, Storage Blob Data Contributor to UAMI
8Private DNS Zonesprivatelink.vaultcore.azure.net, privatelink.blob.core.windows.net, privatelink.<region>.azurecontainerapps.io
9Private EndpointsKey Vault, Storage Account, Container App Environment
10App EnvironmentContainer App Environment (VNet-integrated via subnet delegation)
11ACR Bootstrap BuildDeployment script downloads repo archive via PAT, runs az acr build to push server image
12Front DoorFront Door profile + WAF policy with dashboard and storage origins
13Container AppDashboard app using the ACR-built image, reading secrets from Key Vault via UAMI
14Data DiskManaged disk for the VM
15Recovery VaultRecovery Services Vault for VM backup
16Compute (VM)GPU VM with NVIDIA driver extension + CustomScript bootstrap
17VM BackupEnrolls VM in backup vault

VM Bootstrap (CustomScript extension)​

The CustomScript extension runs scripts/vm-post-setup.sh which:

  1. Prepares the data disk β€” LVM, ext4 format, systemd mount unit
  2. Installs Docker + NVIDIA runtime β€” Docker CE, NVIDIA Container Toolkit, configures Docker data-root on the data disk
  3. Clones the repository β€” Authenticated git clone using the PAT
  4. Writes .env files β€” Generates server/.env, client/.env, db/.env, and redis/.env from deployment parameters (stripping prefixes)
  5. Starts Docker Compose β€” Creates a systemd service that runs docker compose up -d --build

All secrets are passed through the protectedSettings block of the CustomScript extension (encrypted in transit and at rest).

ACR Bootstrap Build​

The scripts/acr-bootstrap-build.sh deployment script:

  1. Downloads a ZIP archive of the repo from Azure DevOps using the PAT
  2. Builds the server Docker image via az acr build
  3. Pushes the resulting image to ACR as <repository>:<tag> (defaults: condor_server:latest)

The Container App references this image on first deployment.

Step 3 β€” Approve Private Endpoint Connections​

After deployment completes, manually approve the Front Door private endpoint connections:

  1. Azure Portal β†’ Container App Environment β†’ Networking β†’ Private Endpoints β†’ Approve the pending Front Door connection
  2. Azure Portal β†’ Storage Account β†’ Networking β†’ Private Endpoints β†’ Approve the pending Front Door connection

Until these are approved, Front Door cannot route traffic to the dashboard or serve blob storage files.

Step 4 β€” Verify​

# Check deployment outputs
az deployment sub show \
-n <deployment-name> \
--query properties.outputs

# Key outputs:
# dashboardEndpointHostName β†’ Front Door URL for the dashboard
# storageEndpointHostName β†’ Front Door URL for blob storage

Open https://<dashboardEndpointHostName> in a browser and log in with the SERVER_SESSION_USERNAME / SERVER_SESSION_PASSWORD credentials.

To verify the VM runtime, connect through Bastion and check:

# On the VM via Bastion
sudo systemctl status condor-compose.service
docker compose -p condor ps
docker compose -p condor logs --tail 50

Step 5 β€” Remote Development with VS Code Tunnels​

After deployment, you can leverage VS Code Remote Tunnels to develop on the GPU VM from anywhere without SSH or RDP. Tunnels provide secure, encrypted access to the VM over the internet without firewall configuration.

Prerequisites for Tunnels​

  • GitHub or Microsoft account β€” Both the VM and your local VS Code client must authenticate with the same account
  • VS Code on the VM β€” Install either full VS Code or the standalone code CLI
  • Internet connectivity β€” Both machines need outbound access to Azure tunneling service (global.rel.tunnels.api.visualstudio.com)

Install VS Code CLI on the VM​

Connect to the VM via Bastion (or SSH) and install the VS Code CLI:

# Option 1: Using the CLI installer (for Linux without UI)
mkdir -p ~/vscode-cli
cd ~/vscode-cli
curl -Lk 'https://code.visualstudio.com/sha/download?build=stable&os=cli-alpine-x64' --output vscode_cli.tar.gz
tar -xf vscode_cli.tar.gz

# Option 2: Download and unpack manually on your client, then upload to VM
# Visit https://code.visualstudio.com/#alt-downloads and download the CLI for your OS

Start a Tunnel​

On the VM, create and start a tunnel:

# If installed via curl:
cd ~/vscode-cli
./code tunnel

# If `code` is in PATH:
code tunnel

You will be prompted to:

  1. Accept the VS Code Server license terms (or pass --accept-server-license-terms to skip)
  2. Authenticate with GitHub/Microsoft β€” Visit the provided authorization URL and sign in

Once authenticated, the CLI outputs a tunnel URL:

https://vscode.dev/tunnel/<machine_name>/<folder>

Copy this URL and open it in your browser from any machine. You can then:

  • Browse the VM's file system
  • Edit code with full IntelliSense
  • Open integrated terminals
  • Run VS Code extensions on the remote VM

Keep the Tunnel Running​

By default, the tunnel stops when you exit the code tunnel command. For persistent access:

# Option 1: Install as a systemd service (recommended)
code tunnel service install
code tunnel service start

# Option 2: Run in the background (no sleep)
code tunnel --no-sleep &

To check tunnel status or view the current tunnel URL:

code tunnel --status

Use Remote Tunnels Extension (Alternative)​

Instead of the CLI, you can enable tunneling through VS Code's UI:

  1. Open VS Code on the VM (full desktop version)
  2. Click the Account menu in the lower left
  3. Select "Turn on Remote Tunnel Access"
  4. Authenticate with GitHub
  5. VS Code will output a tunnel URL in a notification

Connect from a Client​

From your local machine (with VS Code installed or just a browser):

  1. In VS Code: Install the Remote - Tunnels extension
  2. Or: Simply open the tunnel URL in your browser at vscode.dev
  3. Sign in with the same GitHub/Microsoft account used on the VM
  4. Select the remote machine from the tunnel list

Security Notes​

  • Authentication: Both endpoints must authenticate with the same GitHub or Microsoft account
  • Encryption: All data is encrypted with AES-256-CTR over the tunnel
  • No firewall changes needed: Tunnels use outbound connections only
  • Single user: Only one client can access a tunnel instance at a time
  • Limits: Your account can have up to 10 active tunnels; oldest unused tunnels are auto-removed

Useful Commands​

# View all tunnel CLI options
code tunnel --help

# Show tunnel status and URL
code tunnel --status

# Unregister this machine from tunneling
code tunnel unregister

# Stop a running tunnel
# (Press Ctrl+C in the terminal)

# Stop the service
code tunnel service stop
code tunnel service uninstall

Redeployment​

Full redeployment​

Run the same az deployment sub create command. Bicep handles incremental updates β€” unchanged resources are left in place.

Force VM re-bootstrap​

Increment bootstrapTimestamp in parameters.bicepparam and redeploy. This triggers the CustomScript extension to re-run, which:

  • Pulls latest code from the branch
  • Rewrites all .env files
  • Rebuilds and restarts Docker Compose services

Update Container App only​

Increment bootstrapTimestamp to trigger a new ACR build, then redeploy. The Container App picks up the new image.

Continuous Integration (Pipeline)​

The Azure DevOps pipeline azure/pipelines/dashboard.yml handles ongoing server updates:

Code commit (server/**) β†’ Build image β†’ Push to ACR β†’ Manual approval β†’ Update Container App
StepDetails
TriggerCommits to main branch affecting server/**
BuildDocker build from server/Dockerfile, tagged with $(Build.BuildId)
PushImage pushed to ACR
ApprovalManual validation gate (24h timeout)
Deployaz containerapp update with the new image tag

Pipeline Variable Group​

Configure the CONDOR-dev variable group in Azure DevOps (Pipelines β†’ Library):

VariablePurpose
containerRegistryACR name
dashboardImageImage repository name
subscriptionConnectionAzure service connection
acrConnectionACR service connection
notifyUsersApproval notification emails
dashboardAppContainer App resource name
resourceGroupResource group name

File Reference​

FilePurpose
azure/infrastructure/main.bicepSubscription-scoped orchestrator
azure/infrastructure/parameters.example.bicepparamTemplate parameters file (commit-safe)
azure/infrastructure/parameters.bicepparamReal parameters file (git-ignored, contains secrets)
azure/infrastructure/modules/Individual Bicep modules
azure/infrastructure/scripts/vm-post-setup.shVM bootstrap script (CustomScript extension)
azure/infrastructure/scripts/acr-bootstrap-build.shACR build deployment script
azure/pipelines/dashboard.ymlCI/CD pipeline for server updates
docker-compose.yamlCompose stack run on the VM

Troubleshooting​

Deployment fails at ACR bootstrap build​

  • Verify bootstrapRepoPat has read access to the repository
  • Verify bootstrapRepoUrl is a valid Azure DevOps git URL
  • Check the deployment script logs: Azure Portal β†’ Resource Group β†’ Deployments β†’ acrBootstrapBuild

VM CustomScript extension fails​

  • Check extension status: Azure Portal β†’ VM β†’ Extensions β†’ CustomScript
  • SSH via Bastion and inspect logs: /var/log/azure/custom-script/handler.log
  • Verify all required environment variables are set in parameters.bicepparam

Container App shows unhealthy​

  • Verify the ACR image was successfully built and pushed
  • Check Container App logs: az containerapp logs show --name <app> --resource-group <rg>
  • Ensure private endpoint connections are approved (Step 3)

Front Door returns 502/503​

  • Approve pending private endpoint connections (Step 3)
  • Wait up to 10 minutes for Front Door routes to propagate after approval