CI/CD with Azure DevOps
Set up Azure DevOps pipelines for automatic deployments to App Service. Deployment Overview
Deploy FraiseQL on Azure using App Service, Container Apps, Container Instances, or Kubernetes Service (AKS). FraiseQL supports both Azure Database for PostgreSQL and Azure SQL Database (SQL Server) as first-class targets.
If your organization runs SQL Server, Azure SQL Database is the native Azure target — no PostgreSQL migration required.
RESOURCE_GROUP=fraiseql-rgREGION=eastusSQL_SERVER=fraiseql-sql
# Create resource groupaz group create --name $RESOURCE_GROUP --location $REGION
# Create SQL Server (logical server)az sql server create \ --name $SQL_SERVER \ --resource-group $RESOURCE_GROUP \ --location $REGION \ --admin-user fraiseql_admin \ --admin-password "$(openssl rand -base64 20 | tr -d /+= | head -c20)Aa1!"
# Create database (Serverless for dev, General Purpose for prod)az sql db create \ --resource-group $RESOURCE_GROUP \ --server $SQL_SERVER \ --name fraiseql_db \ --edition GeneralPurpose \ --compute-model Serverless \ --family Gen5 \ --min-capacity 1 \ --capacity 4 \ --auto-pause-delay 60 # minutes; set -1 to disable for prodZero-credential authentication — no password in config files or environment variables:
# Assign system-managed identity to Container Appaz containerapp identity assign \ --name fraiseql-app \ --resource-group $RESOURCE_GROUP \ --system-assigned
# Get the managed identity's object IDIDENTITY_OID=$(az containerapp identity show \ --name fraiseql-app \ --resource-group $RESOURCE_GROUP \ --query principalId -o tsv)
# Set Azure AD admin on the SQL server (required before creating AD users)az sql server ad-admin create \ --resource-group $RESOURCE_GROUP \ --server $SQL_SERVER \ --display-name "fraiseql-aad-admin" \ --object-id $IDENTITY_OIDThen connect as an Azure AD admin and create the SQL login for the managed identity:
-- Run against fraiseql_db (not master)CREATE USER [fraiseql-app] FROM EXTERNAL PROVIDER;ALTER ROLE db_datareader ADD MEMBER [fraiseql-app];ALTER ROLE db_datawriter ADD MEMBER [fraiseql-app];[database]# Managed Identity — no passwordurl = "server=fraiseql-sql.database.windows.net;database=fraiseql_db;Authentication=ActiveDirectoryMsi"pool_min = 2pool_max = 20connect_timeout_ms = 5000ssl_mode = "require"# Create Container Apps environmentaz containerapp env create \ --name fraiseql-env \ --resource-group $RESOURCE_GROUP \ --location $REGION
# Deploy FraiseQLaz containerapp create \ --name fraiseql-app \ --resource-group $RESOURCE_GROUP \ --environment fraiseql-env \ --image ghcr.io/fraiseql/fraiseql:latest \ --env-vars \ "DATABASE_URL=server=fraiseql-sql.database.windows.net;database=fraiseql_db;Authentication=ActiveDirectoryMsi" \ --target-port 8080 \ --ingress external \ --min-replicas 1 \ --max-replicas 10# Create Log Analytics workspaceaz monitor log-analytics workspace create \ --resource-group $RESOURCE_GROUP \ --workspace-name fraiseql-logs
# Link to Container App environmentWORKSPACE_ID=$(az monitor log-analytics workspace show \ --resource-group $RESOURCE_GROUP \ --workspace-name fraiseql-logs \ --query customerId -o tsv)
# Query FraiseQL logsaz monitor log-analytics query \ --workspace $WORKSPACE_ID \ --analytics-query "ContainerAppConsoleLogs_CL | where ContainerName_s == 'fraiseql' | order by TimeGenerated desc | take 50"# Set variablesRESOURCE_GROUP=fraiseql-rgREGION=eastusREGISTRY_NAME=fraiseqlregistry
# Create resource groupaz group create \ --name $RESOURCE_GROUP \ --location $REGION
# Create container registryaz acr create \ --resource-group $RESOURCE_GROUP \ --name $REGISTRY_NAME \ --sku Standard
# Build and push Docker imageaz acr build \ --registry $REGISTRY_NAME \ --image fraiseql:latest .
# Get login credentialsaz acr credential show \ --name $REGISTRY_NAME# Create PostgreSQL serveraz postgres server create \ --resource-group $RESOURCE_GROUP \ --name fraiseql-prod \ --location $REGION \ --admin-user fraiseql \ --admin-password "$(openssl rand -base64 32)" \ --sku-name B_Gen5_1 \ --storage-size 51200 \ --backup-retention 30 \ --geo-redundant-backup Enabled
# Create databaseaz postgres db create \ --resource-group $RESOURCE_GROUP \ --server-name fraiseql-prod \ --name fraiseql
# Configure firewall to allow Azure servicesaz postgres server firewall-rule create \ --resource-group $RESOURCE_GROUP \ --server-name fraiseql-prod \ --name AllowAzureServices \ --start-ip-address 0.0.0.0 \ --end-ip-address 0.0.0.0
# Get connection stringaz postgres server show-connection-string \ --server-name fraiseql-prod \ --admin-user fraiseql# Create App Service Planaz appservice plan create \ --name fraiseql-plan \ --resource-group $RESOURCE_GROUP \ --is-linux \ --sku P1V2 \ --number-of-workers 3# Create web appaz webapp create \ --resource-group $RESOURCE_GROUP \ --plan fraiseql-plan \ --name fraiseql-prod \ --deployment-container-image-name \ $REGISTRY_NAME.azurecr.io/fraiseql:latest
# Configure container registryaz webapp config container set \ --name fraiseql-prod \ --resource-group $RESOURCE_GROUP \ --docker-custom-image-name $REGISTRY_NAME.azurecr.io/fraiseql:latest \ --docker-registry-server-url https://$REGISTRY_NAME.azurecr.io \ --docker-registry-server-user $(az acr credential show -n $REGISTRY_NAME --query username -o tsv) \ --docker-registry-server-password $(az acr credential show -n $REGISTRY_NAME --query 'passwords[0].value' -o tsv)# Set application settingsaz webapp config appsettings set \ --resource-group $RESOURCE_GROUP \ --name fraiseql-prod \ --settings \ ENVIRONMENT=production \ LOG_LEVEL=info \ LOG_FORMAT=json \ WEBSITES_ENABLE_APP_SERVICE_STORAGE=false \ DOCKER_ENABLE_CI=true \ DOCKER_REGISTRY_SERVER_URL=https://$REGISTRY_NAME.azurecr.io
# Set secrets (from Key Vault)az keyvault secret set \ --vault-name fraiseql-kv \ --name DatabaseUrl \ --value "postgresql://fraiseql:password@fraiseql-prod.postgres.database.azure.com:5432/fraiseql"
az keyvault secret set \ --vault-name fraiseql-kv \ --name JwtSecret \ --value "$(openssl rand -base64 32)"# Enable health checkaz webapp config set \ --resource-group $RESOURCE_GROUP \ --name fraiseql-prod \ --generic-configurations '{"HealthCheckPath": "/health/ready"}'# Create auto-scale settingsaz monitor autoscale create \ --resource-group $RESOURCE_GROUP \ --resource fraiseql-prod \ --resource-type "Microsoft.Web/sites" \ --name fraiseql-autoscale \ --min-count 3 \ --max-count 10 \ --count 3
# Add scale-up rule (CPU > 70%)az monitor autoscale rule create \ --resource-group $RESOURCE_GROUP \ --autoscale-name fraiseql-autoscale \ --condition "Percentage CPU > 70 avg 5m" \ --scale out 1
# Add scale-down rule (CPU < 30%)az monitor autoscale rule create \ --resource-group $RESOURCE_GROUP \ --autoscale-name fraiseql-autoscale \ --condition "Percentage CPU < 30 avg 5m" \ --scale in 1# Create Key Vaultaz keyvault create \ --resource-group $RESOURCE_GROUP \ --name fraiseql-kv \ --location $REGION
# Create secretsaz keyvault secret set \ --vault-name fraiseql-kv \ --name database-url \ --value "postgresql://user:pass@host/db"
az keyvault secret set \ --vault-name fraiseql-kv \ --name jwt-secret \ --value "$(openssl rand -base64 32)"
az keyvault secret set \ --vault-name fraiseql-kv \ --name cors-origins \ --value "https://app.example.com"# Create managed identityaz identity create \ --resource-group $RESOURCE_GROUP \ --name fraiseql-identity
# Get identity IDIDENTITY_ID=$(az identity show \ --resource-group $RESOURCE_GROUP \ --name fraiseql-identity \ --query id -o tsv)
# Assign to App Serviceaz webapp identity assign \ --resource-group $RESOURCE_GROUP \ --name fraiseql-prod \ --identities $IDENTITY_ID
# Grant Key Vault accessaz keyvault set-policy \ --name fraiseql-kv \ --object-id $(az identity show \ --resource-group $RESOURCE_GROUP \ --name fraiseql-identity \ --query principalId -o tsv) \ --secret-permissions getIn your FraiseQL code, use managed identity:
from azure.identity import DefaultAzureCredentialfrom azure.keyvault.secrets import SecretClient
credential = DefaultAzureCredential()client = SecretClient( vault_url="https://fraiseql-kv.vault.azure.net/", credential=credential)
database_url = client.get_secret("database-url").valuejwt_secret = client.get_secret("jwt-secret").valueOr set App Service app settings to reference Key Vault:
az webapp config appsettings set \ --resource-group $RESOURCE_GROUP \ --name fraiseql-prod \ --settings \ DATABASE_URL="@Microsoft.KeyVault(SecretUri=https://fraiseql-kv.vault.azure.net/secrets/database-url/)" \ JWT_SECRET="@Microsoft.KeyVault(SecretUri=https://fraiseql-kv.vault.azure.net/secrets/jwt-secret/)"For complex workloads requiring Kubernetes:
# Create AKS clusteraz aks create \ --resource-group $RESOURCE_GROUP \ --name fraiseql-aks \ --node-count 3 \ --vm-set-type VirtualMachineScaleSets \ --load-balancer-sku standard \ --enable-managed-identity \ --network-plugin azure \ --enable-addons monitoring,azure-policy \ --enable-app-routing \ --docker-bridge-address 172.17.0.1/16 \ --service-principal ... \ --client-secret ...
# Get credentialsaz aks get-credentials \ --resource-group $RESOURCE_GROUP \ --name fraiseql-aks \ --overwrite-existingFollow the Kubernetes deployment guide with Azure-specific steps:
apiVersion: apps/v1kind: Deploymentmetadata: name: fraiseqlspec: replicas: 3 template: spec: serviceAccountName: fraiseql containers: - name: fraiseql image: fraiseqlregistry.azurecr.io/fraiseql:latest resources: requests: cpu: 500m memory: 512Mi limits: cpu: 2000m memory: 2Gi env: - name: DATABASE_URL valueFrom: secretKeyRef: name: fraiseql-secrets key: database-url - name: JWT_SECRET valueFrom: secretKeyRef: name: fraiseql-secrets key: jwt-secret
---apiVersion: v1kind: Servicemetadata: name: fraiseqlspec: type: LoadBalancer ports: - port: 80 targetPort: 8000 selector: app: fraiseqlDeploy:
# Create secretskubectl create secret generic fraiseql-secrets \ --from-literal=database-url="postgresql://user:pass@host/db" \ --from-literal=jwt-secret="$(openssl rand -base64 32)"
# Deploykubectl apply -f fraiseql-aks-deployment.yaml
# Check statuskubectl get serviceskubectl get pods# Create Application Gatewayaz network application-gateway create \ --name fraiseql-gateway \ --location $REGION \ --resource-group $RESOURCE_GROUP \ --vnet-name fraiseql-vnet \ --subnet gateway-subnet \ --capacity 2 \ --sku Standard_v2 \ --http-settings-cookie-based-affinity Disabled \ --frontend-port 443 \ --http-settings-port 8000 \ --http-settings-protocol HttpFor zero-downtime deployments:
# Create staging slotaz webapp deployment slot create \ --resource-group $RESOURCE_GROUP \ --name fraiseql-prod \ --slot staging
# Deploy to stagingaz webapp config container set \ --name fraiseql-prod \ --resource-group $RESOURCE_GROUP \ --slot staging \ --docker-custom-image-name $REGISTRY_NAME.azurecr.io/fraiseql:staging
# Swap to production (when ready)az webapp deployment slot swap \ --resource-group $RESOURCE_GROUP \ --name fraiseql-prod \ --slot stagingtrigger: - main
pool: vmImage: 'ubuntu-latest'
stages: - stage: Build jobs: - job: BuildAndPush steps: - task: Docker@2 inputs: containerRegistry: 'fraiseql-acr' repository: 'fraiseql' command: 'buildAndPush' Dockerfile: 'Dockerfile' tags: '$(Build.BuildId)'
- stage: Deploy dependsOn: Build jobs: - deployment: DeployToAppService environment: production strategy: runOnce: deploy: steps: - task: AzureAppServiceSettings@1 inputs: azureSubscription: 'Azure Subscription' appName: 'fraiseql-prod' resourceGroupName: $RESOURCE_GROUP
- task: AzureRmWebAppDeployment@4 inputs: azureSubscription: 'Azure Subscription' appType: 'webAppContainer' WebAppName: 'fraiseql-prod'# Create action group for alertsaz monitor action-group create \ --name fraiseql-alerts \ --resource-group $RESOURCE_GROUP
# Create metric alert (high CPU)az monitor metrics alert create \ --name fraiseql-high-cpu \ --resource-group $RESOURCE_GROUP \ --scopes /subscriptions/{subid}/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Web/sites/fraiseql-prod \ --description "Alert when CPU > 70%" \ --condition "avg Percentage CPU > 70" \ --window-size 5m \ --evaluation-frequency 1m \ --action fraiseql-alerts# Create Application Insightsaz monitor app-insights component create \ --app fraiseql-insights \ --location $REGION \ --resource-group $RESOURCE_GROUP
# Link to App Serviceaz webapp config appsettings set \ --resource-group $RESOURCE_GROUP \ --name fraiseql-prod \ --settings APPINSIGHTS_INSTRUMENTATIONKEY=$(az monitor app-insights component show \ --app fraiseql-insights \ --resource-group $RESOURCE_GROUP \ --query instrumentationKey -o tsv)# Create Log Analytics workspaceaz monitor log-analytics workspace create \ --resource-group $RESOURCE_GROUP \ --workspace-name fraiseql-logs
# View logsaz monitor log-analytics query \ --workspace fraiseql-logs \ --analytics-query "ContainerLog | where time > ago(1h)"# View automatic backupsaz postgres server backup show \ --resource-group $RESOURCE_GROUP \ --server-name fraiseql-prod
# Restore from backupaz postgres server restore \ --resource-group $RESOURCE_GROUP \ --name fraiseql-restored \ --source-server fraiseql-prod \ --restore-point-in-time "2024-01-15T12:00:00"
# Enable geo-redundant backups (already done in creation)# Allows restore in different region if primary region fails# Create read replica in different regionaz postgres server replica create \ --name fraiseql-replica \ --resource-group $RESOURCE_GROUP \ --source-server fraiseql-prod \ --location westus
# Promote replica to standalone (if primary fails)az postgres server promote-replica \ --name fraiseql-replica \ --resource-group $RESOURCE_GROUP# Purchase reserved instances (1-3 year commitment)# Typical savings: 30-60%az reservations reservation list# Use spot instances in AKSaz aks nodepool add \ --resource-group $RESOURCE_GROUP \ --cluster-name fraiseql-aks \ --name spotnodepool \ --priority Spot \ --eviction-policy Delete \ --max-surge 33 \ --max-unavailable 33# View logsaz webapp log tail \ --resource-group $RESOURCE_GROUP \ --name fraiseql-prod
# View deployment logsaz webapp deployment log show \ --resource-group $RESOURCE_GROUP \ --name fraiseql-prod
# Restart appaz webapp restart \ --resource-group $RESOURCE_GROUP \ --name fraiseql-prod# Test connectionpsql "postgresql://fraiseql@fraiseql-prod:password@fraiseql-prod.postgres.database.azure.com:5432/fraiseql"
# Check firewall rulesaz postgres server firewall-rule list \ --resource-group $RESOURCE_GROUP \ --server-name fraiseql-prod# Check image in registryaz acr repository list \ --name $REGISTRY_NAME
# View image tagsaz acr repository show-tags \ --name $REGISTRY_NAME \ --repository fraiseqlCI/CD with Azure DevOps
Set up Azure DevOps pipelines for automatic deployments to App Service. Deployment Overview
Custom Dashboards
Create monitoring dashboards in Azure Monitor for visibility into your deployment. Scaling Guide
Security Center
Review and apply Azure Security Center recommendations for FraiseQL. Deployment Overview
Troubleshooting
Diagnose App Service startup failures, database connectivity, and container issues. Troubleshooting Guide