Azure Hosting & Infrastructure Guide¶
Status: Complete | Last updated: 2026-03-23
1. V8 vs V17 Deployment Comparison¶
Understanding the current v8 deployment model is essential for planning the v17 migration. The table below summarizes the key differences.
| Aspect | V8 (Current) | V17 (Recommended) |
|---|---|---|
| Framework | ASP.NET Framework 4.x | .NET 10 (ASP.NET Core) |
| Build | MSBuild → Web Deploy Package (.zip) | docker build → container image |
| Pipeline model | Single parameterized pipeline (dropdown of 168 CUs) | Per-client pipeline (each repo has its own) |
| Deploy target | Azure Web App (Windows/IIS) | Azure Web App for Containers (Linux) |
| Blue-green | Staging slot → manual validation → swap | Staging slot swap OR container tag rollback |
| Per-client config | web.config XML transforms + variable groups | appsettings.json + environment variables (Key Vault) |
| Post-deploy scripts | 10+ PowerShell scripts (IIS, permissions, uSync cleanup) | None -- container is self-contained |
| Codebase | Single monolithic solution (all 168 CUs) | Each client = thin project from dotnet new progress-cu template |
| Shared code | Compiled into the single solution | NuGet packages (Progress.Baseline.Core, Progress.Baseline.Web) |
| Rollback | Swap slots back (instant) | Swap slots back OR redeploy previous container tag |
V8 Pipeline Details¶
The v8 build pipeline (azure-build-pipelines-webApps.yml) produces a Web Deploy Package:
The v8 release pipeline (azure-release-pipelines-webApps.yml) is parameterized with 168 credit union names. At deploy time, an operator selects which CU to deploy from a dropdown:
parameters:
- name: CreditUnionName
displayName: Credit Union Name
type: string
default: Progressie
values:
- 1stclasscu
- Abbeycu
# ... 168 credit unions total
Each CU references two variable groups: Umbraco-Common-PROD (shared settings) and Umbraco-{CreditUnion} (per-client connection strings, domains, etc.).
The release flow is:
- Start staging slot for the selected CU's Azure Web App
- Pre-deployment scripts (export variables, Kudu remote scripts)
- Deploy .zip to staging slot with XML transforms and variable substitution
- Post-deployment scripts (cleanup, permissions)
- Manual validation -- 30-day timeout, operator inspects staging URL
- Swap staging to production -- instant, zero-downtime
V17 Pipeline Architecture¶
Each client is a separate git repo created from the dotnet new progress-cu template. The repo contains only:
appsettings.json(client-specific configuration)Dockerfile(standard, identical across clients)- CSS/theme overrides
azure-pipelines.yml(client-specific pipeline)
All shared functionality comes from NuGet packages referenced in the project.
graph LR
subgraph "Per-Client Git Repo"
REPO["Client X Repo<br/>(from dotnet template)"]
end
subgraph "Azure DevOps"
PIPE["Pipeline<br/>(per-client)"]
end
subgraph "Azure Container Registry"
ACR["progressacr.azurecr.io<br/>/client-x:build-id"]
end
subgraph "Azure"
WEB["Web App for Containers<br/>(client-x)"]
end
subgraph "NuGet Feed"
NUGET["Progress.Baseline.Core<br/>Progress.Baseline.Web"]
end
REPO --> PIPE
PIPE -->|docker build + push| ACR
ACR -->|pull image| WEB
NUGET -->|restored during<br/>docker build| PIPE
Advantages of the V17 Approach¶
- Independent deployments -- deploying one client cannot block or break another
- Container isolation -- no shared app pool; one client crashing does not affect others
- Easy rollback -- switch container image tag to a previous build number (2-minute recovery)
- Consistent environments -- the same container runs locally, in staging, and in production
- Faster deployments -- pull a pre-built image vs deploy + transform a .zip package
- Horizontal scaling -- scale individual client Web Apps independently via multiple container instances
- No post-deploy scripts -- the container is fully self-contained; no IIS permissions, uSync cleanup, or config transforms needed
- Simplified pipeline -- each client pipeline is ~40 lines of YAML vs the v8's 500-line parameterized pipeline
2. Azure Infrastructure¶
Resource Organization¶
Azure Subscription
├── Resource Group: rg-progress-shared
│ ├── Azure Container Registry (ACR)
│ ├── Application Insights (shared workspace)
│ └── Azure Key Vault
├── Resource Group: rg-progress-{client} (one per credit union)
│ ├── Web App for Containers
│ ├── Azure SQL Database (or Elastic Pool member)
│ └── Managed Certificate (SSL)
└── ...
Multi-client pattern
Each credit union gets its own Resource Group containing a Web App for Containers instance and a database. Shared infrastructure (ACR, Key Vault, App Insights) lives in a shared resource group.
Recommended Azure Services¶
| Service | Purpose | SKU Recommendation |
|---|---|---|
| Azure Container Registry | Store per-client Docker images | Basic (sufficient for most; Standard if >100 images or geo-replication needed) |
| Azure Web App for Containers | Host each client site | B1 (basic) or P1v3 (production with auto-scale) |
| Azure SQL Database | Managed database per client | See Database section for options |
| Azure Blob Storage | Media files (optional) | Standard LRS |
| Application Insights | Monitoring and telemetry | Pay-as-you-go |
| Azure Key Vault | Connection strings and secrets | Standard |
3. Container Registry (ACR)¶
Setup¶
# Create ACR (Basic tier is fine for most setups)
az acr create \
--resource-group rg-progress-shared \
--name progressacr \
--sku Basic \
--admin-enabled true
# Get login credentials
az acr credential show --name progressacr
Image Strategy: One Image Per Client¶
Each client repo produces its own Docker image. This ensures complete isolation -- updating one client's NuGet dependencies or theme does not affect any other client.
progressacr.azurecr.io/
├── progresscu:20260323.1
├── progresscu:latest
├── abbeycu:20260323.1
├── abbeycu:latest
├── cardiffcu:20260322.3
├── cardiffcu:latest
└── ...
Image Tagging Strategy¶
| Tag | Purpose | Example |
|---|---|---|
latest |
Most recent successful build | Updated on every main branch push |
{build-number} |
Immutable version for production | 20260323.1 |
Production images
Never deploy latest to production. Use immutable build-number tags for production deployments to enable reliable rollbacks.
4. Per-Client Pipeline¶
Each client repo contains its own azure-pipelines.yml. Since the repo is generated from the dotnet new progress-cu template, the pipeline YAML is identical across clients -- only the variable values differ.
Example Pipeline¶
trigger:
- main
variables:
containerRegistry: "progressacr" # ACR service connection name
imageRepository: "progresscu" # Client-specific image name
tag: "$(Build.BuildId)"
azureSubscription: "progress-azure-sub"
webAppName: "progresscu-v17"
resourceGroup: "rg-progress-progresscu"
stages:
- stage: Build
jobs:
- job: BuildAndPush
pool:
vmImage: ubuntu-latest
steps:
- task: Docker@2
displayName: Build and push container image
inputs:
containerRegistry: $(containerRegistry)
repository: $(imageRepository)
command: buildAndPush
Dockerfile: Dockerfile
tags: |
$(tag)
latest
arguments: >-
--build-arg ASPNETCORE_ENVIRONMENT=Production
--build-arg CONFIGURATION=Release
- stage: DeployStaging
dependsOn: Build
jobs:
- deployment: Deploy
environment: $(webAppName)-staging
strategy:
runOnce:
deploy:
steps:
- task: AzureWebAppContainer@1
inputs:
azureSubscription: $(azureSubscription)
appName: $(webAppName)
deployToSlotOrASE: true
slotName: staging
containers: >-
progressacr.azurecr.io/$(imageRepository):$(tag)
- stage: SwapToProduction
dependsOn: DeployStaging
jobs:
- job: ManualValidation
pool: server
steps:
- task: ManualValidation@0
inputs:
instructions: >
Validate the staging deployment at
https://$(webAppName)-staging.azurewebsites.net
- deployment: Swap
dependsOn: ManualValidation
environment: $(webAppName)-production
strategy:
runOnce:
deploy:
steps:
- task: AzureAppServiceManage@0
inputs:
azureSubscription: $(azureSubscription)
WebAppName: $(webAppName)
ResourceGroupName: $(resourceGroup)
SourceSlot: staging
SwapWithProduction: true
Rollback Strategy¶
| Method | Speed | When to Use |
|---|---|---|
| Slot swap back | Instant | Previous slot still running -- just swap again |
| Redeploy previous tag | 2-5 min | Update container image to previous build number |
| ACR image restore | 5-10 min | If latest was overwritten, pull from ACR by digest |
# Instant rollback via slot swap
az webapp deployment slot swap \
--name progresscu-v17 \
--resource-group rg-progress-progresscu \
--slot staging
# Rollback to specific image version
az webapp config container set \
--name progresscu-v17 \
--resource-group rg-progress-progresscu \
--container-image-name progressacr.azurecr.io/progresscu:20260320.5
5. Web App Configuration¶
Create Web App for Containers¶
# Create App Service Plan (Linux, container-capable)
az appservice plan create \
--name asp-progress-prod \
--resource-group rg-progress-shared \
--is-linux \
--sku P1v3
# Create Web App for a specific client
az webapp create \
--resource-group rg-progress-progresscu \
--plan asp-progress-prod \
--name progresscu-v17 \
--deployment-container-image-name progressacr.azurecr.io/progresscu:latest
Configure ACR Access¶
az webapp config container set \
--name progresscu-v17 \
--resource-group rg-progress-progresscu \
--container-image-name progressacr.azurecr.io/progresscu:latest \
--container-registry-url https://progressacr.azurecr.io \
--container-registry-user progressacr \
--container-registry-password <acr-password>
Environment Variables¶
Secrets and connection strings are injected as environment variables, not baked into the image. Use Key Vault references for sensitive values.
az webapp config appsettings set \
--name progresscu-v17 \
--resource-group rg-progress-progresscu \
--settings \
ASPNETCORE_ENVIRONMENT=Production \
WEBSITES_PORT=8080 \
ConnectionStrings__umbracoDbDSN="@Microsoft.KeyVault(SecretUri=https://kv-progress.vault.azure.net/secrets/progresscu-dbconnection/)" \
ConnectionStrings__umbracoDbDSN_ProviderName="Microsoft.Data.SqlClient" \
Umbraco__CMS__Unattended__InstallUnattended=true \
Umbraco__CMS__Unattended__UpgradeUnattended=true \
Umbraco__CMS__Global__UseHttps=true
Secrets management
Use Azure Key Vault references instead of plaintext passwords in App Settings:
Custom Domain + SSL¶
# Add custom domain
az webapp config hostname add \
--webapp-name progresscu-v17 \
--resource-group rg-progress-progresscu \
--hostname www.progresscu.ie
# Create managed SSL certificate (free)
az webapp config ssl create \
--name progresscu-v17 \
--resource-group rg-progress-progresscu \
--hostname www.progresscu.ie
# Bind SSL
az webapp config ssl bind \
--name progresscu-v17 \
--resource-group rg-progress-progresscu \
--certificate-thumbprint <thumbprint> \
--ssl-type SNI
Health Check¶
az webapp config set \
--name progresscu-v17 \
--resource-group rg-progress-progresscu \
--generic-configurations '{"healthCheckPath": "/umbraco/api/keepalive/ping"}'
Continuous Deployment (Webhook)¶
Enable automatic redeployment when a new image is pushed to ACR:
# Enable continuous deployment
az webapp deployment container config \
--name progresscu-v17 \
--resource-group rg-progress-progresscu \
--enable-cd true
# Get the webhook URL
az webapp deployment container show-cd-url \
--name progresscu-v17 \
--resource-group rg-progress-progresscu
Then configure the ACR webhook to call this URL on image push.
6. Database Options¶
The database choice depends on your budget, performance needs, and operational preferences. Below are the main options.
Option A: Azure SQL Database (Recommended)¶
Fully managed, automatic backups, built-in HA. Best for production workloads with minimal DBA overhead.
# Create SQL Server
az sql server create \
--name progress-sql \
--resource-group rg-progress-shared \
--location westeurope \
--admin-user umbraco_admin \
--admin-password '<strong-password>'
# Create database for a client
az sql db create \
--resource-group rg-progress-shared \
--server progress-sql \
--name progresscu \
--service-objective S0 \
--backup-storage-redundancy Local
Cost optimization with Elastic Pools: With 160+ databases, most of which are low-traffic, an Elastic Pool shares DTU/vCore capacity across all databases and can reduce costs by ~70% compared to individual S0 databases.
az sql elastic-pool create \
--resource-group rg-progress-shared \
--server progress-sql \
--name pool-progress \
--edition Standard \
--dtu 200 \
--db-dtu-max 50 \
--db-dtu-min 0
Option B: SQL Server on a VM¶
Self-managed SQL Server on an Azure VM. Lower per-database cost at scale but requires manual backups, patching, and HA setup.
Option C: Azure SQL Managed Instance¶
Middle ground -- SQL Server feature compatibility with managed infrastructure. Higher base cost but simpler migration from on-premises SQL Server.
Connection String Format¶
Server=tcp:progress-sql.database.windows.net,1433;
Database=progresscu;
User Id=umbraco_user;
Password=<password>;
Encrypt=True;
TrustServerCertificate=False;
MultipleActiveResultSets=true
Provider name
Always set both the connection string AND the provider name:
Firewall Rules (Azure SQL)¶
# Allow Azure services
az sql server firewall-rule create \
--resource-group rg-progress-shared \
--server progress-sql \
--name AllowAzureServices \
--start-ip-address 0.0.0.0 \
--end-ip-address 0.0.0.0
# Allow specific IP (office/VPN)
az sql server firewall-rule create \
--resource-group rg-progress-shared \
--server progress-sql \
--name OfficeAccess \
--start-ip-address 203.0.113.10 \
--end-ip-address 203.0.113.10
Backup Strategy (Azure SQL)¶
| Feature | Configuration |
|---|---|
| Point-in-time restore | Automatic, 7-35 days retention (depends on tier) |
| Long-term retention | Weekly full backups, 52-week retention |
| Geo-replication | Optional, for disaster recovery |
az sql db ltr-policy set \
--resource-group rg-progress-shared \
--server progress-sql \
--database progresscu \
--weekly-retention P4W \
--monthly-retention P12M \
--yearly-retention P5Y \
--week-of-year 1
7. Multi-Client Management¶
Shared vs Per-Client Resources¶
| Resource | Shared | Per-Client |
|---|---|---|
| App Service Plan | Shared plan (cost optimization) | Own Web App instance |
| SQL Server | Shared server / Elastic Pool | Own database |
| ACR | Single registry | Own image repository |
| NuGet packages | Shared feed | -- |
| Application Insights | Can share workspace | Own instrumentation key |
| Key Vault | Shared vault | Own secrets |
| Custom domain | -- | Own domain + SSL |
| Git repo | -- | Own repo (from template) |
| Pipeline | -- | Own pipeline |
Onboarding a New Client¶
- Create project from template:
dotnet new progress-cu --name NewClientCu - Customize: Edit
appsettings.json(site name, theme, features), add CSS overrides - Create Azure resources: Resource group, Web App, SQL Database, Key Vault secrets
- Create pipeline: Push to Azure DevOps, pipeline YAML is already in the template
- First deploy: Pipeline builds image, pushes to ACR, deploys to Web App
- DNS: Point client domain to the Web App, bind SSL certificate
App Service Plan Density¶
A single App Service Plan can host multiple Web Apps. The number depends on the plan tier and each client's resource usage.
| Component | What to Size | Considerations |
|---|---|---|
| App Service Plan | CPU, memory per plan | Multiple low-traffic sites can share a plan; high-traffic sites should have a dedicated plan |
| Azure SQL | DTU or vCore per database | Elastic Pools can reduce cost for many small databases |
| ACR | Storage + pull frequency | Basic tier stores up to 10 GB; Standard adds geo-replication |
| Blob Storage | Media volume per client | Pay-per-GB; consider lifecycle policies for old media |
Cost Estimation
Use the Azure Pricing Calculator to build an estimate based on your specific requirements. Key inputs:
- Number of clients to host
- Expected traffic per client (page views/month)
- Database size per client
- Media storage volume
- Desired redundancy (single region vs geo-replicated)
Start small (Basic/B1 plans) and scale up based on actual usage. Azure makes it easy to change tiers without downtime.
8. Cascading Updates — Base Package Changes¶
When the shared baseline code (Progress.Baseline.Core, Progress.Baseline.Web) is updated, all client sites need to receive the update. There are two approaches:
Option A: Manual Update (Simple)¶
Update the NuGet package version in each client project:
# In each client repo
dotnet add package Progress.Baseline.Core --version 1.2.0
dotnet add package Progress.Baseline.Web --version 1.2.0
git commit -am "chore: update baseline packages to 1.2.0"
git push # triggers client pipeline → build → deploy
Best for: Small teams, < 10 clients, infrequent base changes
Option B: Automated Fan-Out Pipeline (Scalable)¶
A central pipeline that automatically triggers all client deployments when new NuGet packages are published:
graph LR
A[Baseline Code Change] --> B[NuGet Pipeline]
B --> C[Publish Packages to Feed]
C --> D[Fan-Out Pipeline Triggers]
D --> E1[Client A Pipeline]
D --> E2[Client B Pipeline]
D --> E3[Client C Pipeline]
D --> E4[Client N Pipeline]
E1 --> F1[Build + Push Image A]
E2 --> F2[Build + Push Image B]
E3 --> F3[Build + Push Image C]
E4 --> F4[Build + Push Image N]
How it works:
- Developer pushes a fix to the baseline repository
- NuGet pipeline builds and publishes new package versions
- Fan-out pipeline detects the new version and triggers each client pipeline
- Each client pipeline:
dotnet restore(pulls new packages) →docker build→ push to ACR → deploy to Web App
Azure DevOps implementation:
# fan-out-pipeline.yml — triggered after NuGet publish
trigger: none # triggered by NuGet pipeline completion
resources:
pipelines:
- pipeline: nuget-publish
source: 'Progress-NuGet-Publish'
trigger:
branches:
include: [main]
jobs:
- job: TriggerClients
pool:
vmImage: ubuntu-latest
strategy:
matrix:
ClientA:
repoName: 'HeritageCrediUnion'
ClientB:
repoName: 'ProgressCU'
ClientC:
repoName: 'CuisoSolutions'
# ... add each client
steps:
- task: TriggerPipeline@1
inputs:
serviceConnection: 'AzureDevOps'
project: '$(repoName)'
definitionName: 'Build-and-Deploy'
buildParameters: 'packageVersion=$(Build.BuildNumber)'
Alternatively, each client pipeline can check for new package versions on a schedule:
# In each client pipeline
schedules:
- cron: '0 6 * * 1-5' # 6am weekdays
displayName: 'Check for baseline updates'
branches:
include: [main]
always: true
steps:
- script: |
# Check if newer baseline packages are available
LATEST=$(dotnet package search Progress.Baseline.Core --source ProgressFeed --format json | jq -r '.searchResult[0].packages[0].latestVersion')
CURRENT=$(dotnet list package --format json | jq -r '.projects[0].frameworks[0].topLevelPackages[] | select(.id=="Progress.Baseline.Core") | .resolvedVersion')
if [ "$LATEST" != "$CURRENT" ]; then
echo "##vso[task.setvariable variable=needsUpdate]true"
dotnet add package Progress.Baseline.Core --version $LATEST
dotnet add package Progress.Baseline.Web --version $LATEST
fi
displayName: 'Check for baseline updates'
Recommendation
Start with Option A during initial rollout (fewer clients, more control). Move to Option B once you have 10+ clients deployed and the baseline packages are stable.
Versioning Strategy¶
| Version Type | When | Example |
|---|---|---|
| Patch (1.0.x) | Bug fixes, no breaking changes | 1.0.1 → auto-deploy safe |
| Minor (1.x.0) | New features, backward compatible | 1.1.0 → test on one client first |
| Major (x.0.0) | Breaking changes | 2.0.0 → staged rollout with testing |
Breaking Changes
Major version updates may require changes in client projects (e.g., renamed services, changed view models). Always test on a single client before fan-out deployment.
9. Monitoring & Maintenance¶
Application Insights¶
az webapp config appsettings set \
--name progresscu-v17 \
--resource-group rg-progress-progresscu \
--settings \
APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=<key>;IngestionEndpoint=https://westeurope-5.in.applicationinsights.azure.com/"
Or via appsettings.json:
{
"ApplicationInsights": {
"ConnectionString": "<from-key-vault>"
},
"Logging": {
"ApplicationInsights": {
"LogLevel": {
"Default": "Warning",
"Umbraco": "Information"
}
}
}
}
Log Streaming¶
# Stream live logs
az webapp log tail \
--name progresscu-v17 \
--resource-group rg-progress-progresscu
# Enable container logging
az webapp log config \
--name progresscu-v17 \
--resource-group rg-progress-progresscu \
--docker-container-logging filesystem
Teams Notifications¶
The per-client pipeline can include a Teams webhook notification step (Adaptive Card) reporting:
- Image name and tag
- Commit SHA, message, and author
- Link to the deployed site and pipeline run
Scaling¶
| Scenario | Solution |
|---|---|
| Single client spike | Scale up App Service Plan tier |
| Many low-traffic clients | Share a single App Service Plan (multiple apps per plan) |
| High-traffic clients | Dedicated App Service Plan with auto-scale |
| Database bottleneck | Scale DTU tier or move to Elastic Pool |
# Auto-scale rule (CPU-based)
az monitor autoscale create \
--resource-group rg-progress-shared \
--resource asp-progress-prod \
--resource-type Microsoft.Web/serverfarms \
--min-count 1 --max-count 5 --count 1
az monitor autoscale rule create \
--resource-group rg-progress-shared \
--autoscale-name <autoscale-name> \
--scale out 1 \
--condition "CpuPercentage > 70 avg 5m"
az monitor autoscale rule create \
--resource-group rg-progress-shared \
--autoscale-name <autoscale-name> \
--scale in 1 \
--condition "CpuPercentage < 30 avg 10m"
Cost Optimization Summary¶
| Strategy | Savings |
|---|---|
| Elastic Pool for 160+ DBs | ~70% vs individual S0 databases |
| Shared App Service Plan | ~80% vs individual plans |
| Reserved instances (1yr) | ~30-40% on compute |
| Basic tier for staging slots | Keep staging stopped when not in use |
| Blob Storage for media | Offload static files from app containers |
Appendix: Dockerfile Reference¶
The application Dockerfile uses a multi-stage build:
# Stage 1: Build with .NET 10 SDK + Node.js 18
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
# Restores NuGet packages (including Progress.Baseline.*), builds with dotnet publish
# Stage 2: Runtime with ASP.NET Core 10
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
# Copies published output, sets ENTRYPOINT ["dotnet", "www.dll"]
Key build arguments:
| Argument | Default | Production |
|---|---|---|
CONFIGURATION |
Debug |
Release |
ASPNETCORE_ENVIRONMENT |
Debug |
Production |
Appendix: Quick Reference Commands¶
# Build and run locally
docker compose -f docker-compose.yml up -d
# Build image manually
docker build -t progresscu:local \
--build-arg CONFIGURATION=Release \
--build-arg ASPNETCORE_ENVIRONMENT=Production \
-f Dockerfile .
# Push to ACR
az acr login --name progressacr
docker tag progresscu:local progressacr.azurecr.io/progresscu:latest
docker push progressacr.azurecr.io/progresscu:latest
# Check deployed image
az webapp config container show \
--name progresscu-v17 \
--resource-group rg-progress-progresscu