Deploy SupportPilot to Azure

Azure Container Apps gives you serverless containers with built-in HTTPS ingress and autoscaling. Azure Database for PostgreSQL stores checkpoints, Key Vault holds secrets, and a managed identity ties it together without passwords in config.

Your progress
GitHub Actions Users (HTTPS) Container Registry (ACR) Container Appmanaged identity · KEDA scale Key Vault PostgreSQL Flexible LLM API / AI Foundry

Prerequisites

Step-by-step

1

Log in, register providers, set variables

az login
az extension add --name containerapp --upgrade
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights

export RG=supportpilot-rg LOC=eastus
export SUFFIX=$RANDOM                       # globally-unique names
export ACR=supportpilot$SUFFIX KV=sp-kv-$SUFFIX PG=sp-pg-$SUFFIX
az group create -n $RG -l $LOC
2

Build the image in ACR

az acr create -g $RG -n $ACR --sku Basic
az acr build -r $ACR -t supportpilot:v1 .     # cloud build, no local Docker needed
3

PostgreSQL Flexible Server

export DB_PASS=$(openssl rand -hex 16)

az postgres flexible-server create -g $RG -n $PG -l $LOC \
  --admin-user agent --admin-password "$DB_PASS" \
  --tier Burstable --sku-name Standard_B1ms --storage-size 32 --version 16 \
  --public-access 0.0.0.0                       # allow Azure services only
az postgres flexible-server db create -g $RG -s $PG -d agent

For production, deploy the server with private access (VNet integration) and put the Container Apps environment in the same VNet.

4

Key Vault and secrets

az keyvault create -g $RG -n $KV -l $LOC --enable-rbac-authorization true

# give yourself permission to write secrets
az role assignment create --assignee $(az ad signed-in-user show --query id -o tsv) \
  --role "Key Vault Secrets Officer" --scope $(az keyvault show -n $KV --query id -o tsv)

az keyvault secret set --vault-name $KV -n anthropic-api-key --value "$ANTHROPIC_API_KEY"
az keyvault secret set --vault-name $KV -n database-url \
  --value "postgresql://agent:$DB_PASS@$PG.postgres.database.azure.com:5432/agent?sslmode=require"
5

Managed identity with least privilege

az identity create -g $RG -n supportpilot-id
export ID_RES=$(az identity show -g $RG -n supportpilot-id --query id -o tsv)
export ID_PRINCIPAL=$(az identity show -g $RG -n supportpilot-id --query principalId -o tsv)

az role assignment create --assignee-object-id $ID_PRINCIPAL --assignee-principal-type ServicePrincipal \
  --role AcrPull --scope $(az acr show -n $ACR --query id -o tsv)
az role assignment create --assignee-object-id $ID_PRINCIPAL --assignee-principal-type ServicePrincipal \
  --role "Key Vault Secrets User" --scope $(az keyvault show -n $KV --query id -o tsv)

The app pulls images and reads secrets as this identity — no registry passwords or secret values stored in the app config.

6

Create the Container Apps environment and app

az containerapp env create -g $RG -n supportpilot-env -l $LOC

export KV_URI=$(az keyvault show -n $KV --query properties.vaultUri -o tsv)

az containerapp create -g $RG -n supportpilot --environment supportpilot-env \
  --image $ACR.azurecr.io/supportpilot:v1 \
  --registry-server $ACR.azurecr.io --registry-identity $ID_RES \
  --user-assigned $ID_RES \
  --target-port 8080 --ingress external \
  --cpu 0.5 --memory 1.0Gi \
  --min-replicas 1 --max-replicas 10 \
  --scale-rule-name http-load --scale-rule-type http --scale-rule-http-concurrency 40 \
  --secrets "anthropic-key=keyvaultref:${KV_URI}secrets/anthropic-api-key,identityref:$ID_RES" \
            "db-url=keyvaultref:${KV_URI}secrets/database-url,identityref:$ID_RES" \
  --env-vars MODEL=anthropic:claude-sonnet-5-5 \
             ANTHROPIC_API_KEY=secretref:anthropic-key \
             DATABASE_URL=secretref:db-url
  • keyvaultref: secrets are fetched by the managed identity and refreshed automatically.
  • The HTTP scale rule adds replicas when concurrent requests per replica exceed 40; set --min-replicas 0 to scale to zero in dev.
  • Ingress gives you a managed HTTPS endpoint out of the box.
7

Smoke test

URL=https://$(az containerapp show -g $RG -n supportpilot --query properties.configuration.ingress.fqdn -o tsv)
curl -s $URL/healthz
curl -s $URL/chat -H 'content-type: application/json' \
  -d '{"thread_id":"az-1","message":"Can I get a refund on a gift card?"}'

az containerapp logs show -g $RG -n supportpilot --follow
Lock it downThe endpoint is public. Add authentication with Container Apps built-in auth (Entra ID), or front it with API Management / Front Door with WAF.
8

CI/CD with GitHub Actions (OIDC)

Create an Entra app registration with a federated credential for your repo's main branch, then grant it Contributor on the resource group and AcrPush on the registry.

APP_ID=$(az ad app create --display-name supportpilot-gh --query appId -o tsv)
az ad sp create --id $APP_ID
az ad app federated-credential create --id $APP_ID --parameters '{
  "name": "gh-main", "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:YOUR_ORG/YOUR_REPO:ref:refs/heads/main",
  "audiences": ["api://AzureADTokenExchange"]}'
az role assignment create --assignee $APP_ID --role Contributor --scope $(az group show -n $RG --query id -o tsv)
az role assignment create --assignee $APP_ID --role AcrPush --scope $(az acr show -n $ACR --query id -o tsv)
name: deploy-azure
on:
  push:
    branches: [main]
permissions:
  contents: read
  id-token: write
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install -r requirements-dev.txt && pytest -q
      - uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
      - run: |
          az acr build -r ${{ vars.ACR_NAME }} -t supportpilot:${{ github.sha }} .
          az containerapp update -g supportpilot-rg -n supportpilot \
            --image ${{ vars.ACR_NAME }}.azurecr.io/supportpilot:${{ github.sha }}

Each update creates a new revision. Use multiple-revision mode with traffic splitting for blue/green or canary releases.

Using Azure-hosted models

Use Azure OpenAI or models from Azure AI Foundry (which also offers Claude) so traffic stays in your tenant:

# requirements.txt: add langchain-openai
# MODEL=azure_openai:<your deployment name>
az containerapp update -g $RG -n supportpilot \
  --set-env-vars MODEL=azure_openai:gpt-deployment \
     AZURE_OPENAI_ENDPOINT=https://YOUR-RESOURCE.openai.azure.com/ \
     OPENAI_API_VERSION=YOUR_API_VERSION

Prefer keyless auth: grant the managed identity the Cognitive Services OpenAI User role and pass an Entra token provider to the model client instead of an API key.

Alternatives on Azure

OptionWhen to choose it
Container Apps (this guide)Default for containerised agent APIs: serverless, KEDA autoscaling, revisions.
Azure AI Foundry Agent ServiceManaged agent runtime with built-in threads, tools and evaluation, tightly integrated with Azure models.
App Service (Linux containers)Teams already standardised on App Service; deployment slots.
AKSKubernetes platform, GPUs, complex networking.
Container Apps jobsScheduled/batch agents and migrations.

GCP vs AWS vs Azure cheat-sheet

ConcernGCPAWSAzure
Serverless containersCloud RunECS Fargate (+ ALB)Container Apps
RegistryArtifact RegistryECRACR
PostgresCloud SQL / AlloyDBRDS / AuroraPostgreSQL Flexible Server
SecretsSecret ManagerSecrets ManagerKey Vault
Workload identityService accountIAM task roleManaged identity
Managed modelsVertex AIBedrockAzure OpenAI / AI Foundry
Managed agent runtimeVertex AI Agent EngineBedrock AgentCoreAI Foundry Agent Service
Vector searchVertex AI Vector SearchOpenSearch / S3 VectorsAzure AI Search
Logs / tracesCloud Logging / TraceCloudWatch / X-RayLog Analytics / App Insights
CI auth from GitHubWorkload Identity FederationIAM OIDC providerEntra federated credential

Clean up

az group delete -n $RG --yes --no-wait     # deletes everything in the resource group
az keyvault purge -n $KV                   # Key Vault is soft-deleted; purge to reuse the name