Detailed Deployment Guide
This guide provides comprehensive deployment instructions, architectural context, and advanced configuration options. For quick-start steps, see the Quick Start Guide.
Important Prerequisites
Prerequisites Checklist
- Azure CLI v2.40+ installed.
- Bicep CLI v0.20+ (included with Azure CLI v2.20+).
- PowerShell 5.1+.
- Azure subscription with Contributor or Owner role.
- Entra ID tenant access.
- Strong admin password for session host VMs.
For detailed setup, see Prerequisites Guide.
Deployment Architecture
Idempotent Design
This template is fully idempotent, meaning:
- Safe redeployment: Running deployment multiple times creates/updates the same resource set without duplicates.
- Stable resource names: Resource names are deterministically derived from the resource group ID using
uniqueString(resourceGroup().id). - Failed deployment recovery: If deployment fails midway, simply rerun—only missing resources will be created.
- CI/CD ready: Suitable for automated deployment pipelines.
Technical implementation:
- All resource names are stable via
uniqueString(). - Idempotent operations use
dependsOnand stable naming. - No random naming that could cause duplicates.
Session Host Extension Architecture
The deployment uses two extensions deployed in sequence on each VM:
1. AADLoginForWindows:
- Publisher: Microsoft.Azure.ActiveDirectory.
- Purpose: Enables Entra ID authentication and login on the VM.
- Allows users to sign in with their Entra ID credentials.
- Deployed first; must complete before DSC extension.
2. DSC (Desired State Configuration):
- Publisher: Microsoft.Powershell.
- Version: 2.73.
- Purpose: Registers the VM as a session host in the AVD host pool using the registration token.
- Runs after AADLoginForWindows completes (dependency enforced).
- Configures the VM to join the host pool with Entra ID authentication enabled.
Benefit: Clean separation of concerns—Entra ID authentication is configured first, then AVD host pool registration follows. Deployment is reliable and works correctly on redeployment.
sequenceDiagram
participant VM as Session Host VM
participant AAD as AADLoginForWindows
participant DSC as DSC Extension
participant HP as Host Pool
participant Storage as Artifact storage
VM->>AAD: Install extension
AAD-->>VM: Entra ID auth enabled
VM->>DSC: Install extension (after AAD)
DSC->>Storage: Download gallery artifact
DSC->>HP: Register with token
HP-->>VM: Session host Available
AVD DSC Artifact URL
The artifactsLocation parameter in infra/parameters.json points to a pinned Microsoft gallery artifact ZIP used by the DSC extension to register session hosts with the host pool:
https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/Configuration_1.0.02774.414.zipThis URL is not auto-updated. Microsoft periodically publishes newer Configuration_1.0.*.zip artifacts with bug fixes and compatibility updates for AVD session host registration.
When to update:
- Session host provisioning fails during the DSC extension step.
- Microsoft documentation or release notes reference a newer required artifact version.
- You are troubleshooting persistent session host registration errors after verifying network and RBAC configuration.
How to check for a newer artifact:
- Review the AVD session host setup documentation for the current recommended artifact.
- Browse the gallery artifacts container parent path for newer
Configuration_1.0.*.zipfiles. - Update
artifactsLocationininfra/parameters.json(or pass-ArtifactsLocationto the deploy script if supported). - Redeploy or re-run the DSC extension on affected session hosts.
- Verify registration with
.\scripts\Test-AvdSessionHost.ps1.
Do not bump the artifact URL without testing a full deployment — newer artifacts may require matching Bicep API versions or extension settings.
Explicit Outbound Connectivity (March 2026 Compliance)
Starting March 31, 2026, Azure requires explicit outbound connectivity methods for new virtual networks. This template implements compliance through:
Implementation
Standard Public IPs on NICs
- Each session host VM gets a Standard Public IP address.
- Public IPs attached to VM network interfaces (not standalone).
- Provides explicit outbound NAT gateway.
Service Endpoints (cost-free optimization)
- Optimised routes to Azure services (Storage, Key Vault, Entra AD).
- Zero additional cost.
- Reduces data egress charges.
Network Defaults
- Subnets have
defaultOutboundAccess: false(explicitly disabled). - Forces all outbound traffic through Public IP (no hidden implicit access).
Public IP Lifecycle
- Initial Deployment: Bicep creates Standard Public IPs and associates them with VM NICs.
- When Stopping VMs:
Stop-AvdOccasional.ps1deallocates VMs, then deletes Public IPs (saves ~£2–3/month per VM). - When Starting VMs:
Start-AvdOccasional.ps1creates new Public IPs, associates them with NICs, then starts VMs. - IP Address Changes: Public IPs get new addresses each start cycle (acceptable and expected for outbound-only connectivity).
Why This Approach:
- Compliant – Meets Azure’s explicit outbound requirement.
- Cost-optimised – £0/month when VMs stopped (vs ~£2–3/month if kept).
- Secure – NSG blocks ALL inbound traffic; IPs are outbound-only.
- Automated – Start/Stop scripts handle entire lifecycle.
- Fully functional – Provides internet access for updates, DSC downloads, browsing.
Step-by-Step Deployment
1. Clone Repository
git clone https://github.com/markheydon/avd-occasional.git
cd avd-occasional2. Verify Tools
# Check Azure CLI version
az --version
# Check Bicep CLI
az bicep version
# Check PowerShell version
$PSVersionTable.PSVersion
# Authenticate to Azure
az login
# Verify subscription
az account show # Ensure correct subscription is selectedIf you have multiple subscriptions, select the target one:
az account set --subscription <subscription-id-or-name>3. Customise Parameters
Edit infra/parameters.json to customize deployment:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environment": { "value": "dev" },
"workloadSize": { "value": "moderate" },
"location": { "value": "ukwest" },
"vmCount": { "value": 1 },
"adminUsername": { "value": "avdadmin" },
"tags": {
"value": {
"project": "avd-occasional",
"managedBy": "bicep"
}
}
}
}Common Customisations:
| Parameter | Options | Default | Cost Impact |
|---|---|---|---|
environment | dev, test, prod | dev | None (affects naming) |
workloadSize | light, moderate | moderate | Light: £35/mo, Moderate: £100–120/mo |
location | Any Azure region | ukwest | Regional pricing variance |
vmCount | 1–5 | 1 | Each VM adds ~£100/mo active |
adminUsername | Any valid Windows name | avdadmin | None |
4. Prepare Admin Password
Create a secure password. Never hardcode it in scripts or parameters.json. Instead, prompt interactively:
# Option 1: Interactive prompt (recommended)
$adminPassword = Read-Host "Enter admin password for session hosts" -AsSecureString
# Option 2: Generate random secure password (PowerShell 5.1+)
Add-Type -AssemblyName System.Web
$adminPassword = ConvertTo-SecureString ([System.Web.Security.Membership]::GeneratePassword(16, 3)) -AsPlainText -Force
# Option 3: Supply via environment variable (for CI/CD)
$adminPassword = ConvertTo-SecureString $env:AVD_ADMIN_PASSWORD -AsPlainText -ForceSecurity note: Passwords marked @secure() in Bicep are never logged to Azure activity logs.
5. Validate Template (Dry Run)
Test the deployment before creating resources:
# Get admin password
$adminPassword = Read-Host "Enter admin password for session hosts" -AsSecureString
# Validate template
az deployment group validate `
--resource-group avd-occasional-rg `
--template-file infra/main.bicep `
--parameters @infra/parameters.json `
--parameters adminPassword=$adminPassword `
-o json | ConvertFrom-JsonOr use the PowerShell script with -WhatIf:
$adminPassword = Read-Host "Enter admin password for session hosts" -AsSecureString
.\scripts\Deploy-AvdOccasional.ps1 -AdminPassword $adminPassword -WhatIfExpected output: Lists resources to be created with no errors.
6. Create Resource Group
az group create `
--name avd-occasional-rg `
--location ukwestAlternatively, let the deployment script create it (steps below).
7. Deploy Infrastructure
# Get admin password
$adminPassword = Read-Host "Enter admin password for session hosts" -AsSecureString
# Deploy (20–30 minutes for first deployment)
.\scripts\Deploy-AvdOccasional.ps1 -AdminPassword $adminPasswordExpected duration:
- Resource group creation: 1–2 minutes
- Template compilation: 1–2 minutes
- Infrastructure deployment: 15–20 minutes
- VM extensions (Entra ID join): 5–10 minutes
- Total: 20–35 minutes
Monitor progress in Azure Portal:
- Go to Resource Groups >
avd-occasional-rg> Deployments - Click latest deployment (“main”) to view status
8. Verify Deployment
# List all resources (should show ~15 resources)
az resource list --resource-group avd-occasional-rg -o table
# Check VM status (should show "running")
az vm list --resource-group avd-occasional-rg `
--query "[].{Name:name, Status:powerState, VMSize:hardwareProfile.vmSize}" -o table
# View deployment outputs
az deployment group show --resource-group avd-occasional-rg `
--name main `
--query properties.outputs -o jsonExpected resources:
- 1 Virtual Network
- 1 Network Security Group
- 1–5 VMs (depending on vmCount parameter)
- 1–5 Network Interfaces
- 1–5 OS Disks
- 1–5 Public IPs
- 1 Host Pool
- 1 Workspace
- 1 Application Group
- 1 System-assigned Managed Identity
9. Configure User Access (Required)
Full instructions: Quick Start: Step 3 or Prerequisites: Role Assignment Requirements.
Quick scripts:
# Role 1: Desktop Virtualization User
$appGroupId = (az resource list --resource-group avd-occasional-rg `
--resource-type "Microsoft.DesktopVirtualization/applicationGroups" `
--query '[0].id' -o tsv)
$userId = (az ad signed-in-user show --query id -o tsv)
az role assignment create `
--role "Desktop Virtualization User" `
--assignee $userId `
--scope $appGroupId
# Role 2: Virtual Machine User Login (or Administrator Login for dev/admin access)
$vmIds = @(az vm list --resource-group avd-occasional-rg --query '[].id' -o tsv)
foreach ($vmId in $vmIds) {
az role assignment create `
--role "Virtual Machine User Login" `
--assignee $userId `
--scope $vmId
}For local administrator rights while signed in with Entra ID, use Virtual Machine Administrator Login instead of User Login. See Quick Start: Role 2, Option B.
10. Test Connection
Using Windows App (Recommended):
- Install Windows App from Microsoft Store
- Launch Windows App
- Sign in with your Entra ID account
- (First time) Subscribe to workspace:
avd-dev(or custom name based on environment parameter) - Click Personal Desktop
- Verify desktop is displayed
Using Remote Desktop Connection (Advanced):
RDP Shortpath requires additional setup (not recommended for occasional use). See Azure Virtual Desktop RDP Properties.
Customising Deployment
Add More Session Hosts (Scale Out)
# Edit parameters.json
"vmCount": { "value": 3 } # Changed from 1 to 3
# Redeploy (idempotent; only new VMs created)
$adminPassword = Read-Host "Enter admin password" -AsSecureString
.\scripts\Deploy-AvdOccasional.ps1 -AdminPassword $adminPassword
# Assign roles to new VMs (use Virtual Machine Administrator Login for admin access)
$userId = (az ad signed-in-user show --query id -o tsv)
$vmIds = @(az vm list --resource-group avd-occasional-rg --query '[].id' -o tsv)
foreach ($vmId in $vmIds) {
az role assignment create `
--role "Virtual Machine User Login" `
--assignee $userId `
--scope $vmId
}See Quick Start: Role 2 if you need administrator rights on new VMs.
Cost impact: Each VM adds ~£100–120/month active, ~£0 deallocated.
Switch Between Light and Moderate Workloads
# Edit parameters.json
"workloadSize": { "value": "light" } # From "moderate" to "light"
# Redeploy
$adminPassword = Read-Host "Enter admin password" -AsSecureString
.\scripts\Deploy-AvdOccasional.ps1 -AdminPassword $adminPasswordCost impact:
- Light (B2s): ~£35/month active
- Moderate (D2s_v5): ~£100–120/month active
- Deallocated: ~£0 (same for both)
Note: Existing VMs are replaced (not upgraded in-place). Redeploy may take longer as old VMs are removed first.
Change Azure Region
# Edit parameters.json
"location": { "value": "northeurope" } # From "ukwest" to europe
# Redeploy
$adminPassword = Read-Host "Enter admin password" -AsSecureString
.\scripts\Deploy-AvdOccasional.ps1 -AdminPassword $adminPasswordSupported regions:
- UK:
uksouth,ukwest - Europe:
northeurope,westeurope - Americas:
eastus,westus2,canadacentral
See Azure Virtual Desktop Supported Regions.
Use Custom Tags for Billing
# Edit parameters.json
"tags": {
"value": {
"project": "avd-occasion",
"team": "engineering",
"cost-center": "1234",
"environment": "dev"
}
}
# Redeploy
$adminPassword = Read-Host "Enter admin password" -AsSecureString
.\scripts\Deploy-AvdOccasional.ps1 -AdminPassword $adminPasswordTags are used for cost analysis and resource filtering in Azure Cost Management.
Troubleshooting Deployment
General Debugging
# Run with verbose output
$adminPassword = Read-Host "Enter password" -AsSecureString
.\scripts\Deploy-AvdOccasional.ps1 -AdminPassword $adminPassword -Verbose
# Check latest deployment status
az deployment group show --resource-group avd-occasional-rg --name main -o json | ConvertFrom-Json | .propertiesCommon Errors
See Troubleshooting Guide: Deployment Issues for:
- “Subscription does not have quota”
- “InvalidTemplateDeployment”
- “User does not have permissions”
- “Template contains invalid syntax”
- “Location not supported”
Lifecycle Management
Deallocate to Save Costs
# Stop VMs and delete Public IPs (~98% cost savings)
.\scripts\Stop-AvdOccasional.ps1Monthly cost drops from £94–126 to £2–3. Data and configuration preserved.
Resume After Deallocate
# Start VMs and recreate Public IPs (2–3 minute startup)
.\scripts\Start-AvdOccasional.ps1 -WaitForStartupCompletely Delete Infrastructure
# Permanently delete all resources (cannot be undone)
.\scripts\Remove-AvdOccasional.ps1 -ForceDeploy Again After Deletion
# Redeploy fresh infrastructure
$adminPassword = Read-Host "Enter admin password" -AsSecureString
.\scripts\Deploy-AvdOccasional.ps1 -AdminPassword $adminPasswordCost Analysis
Component Costs (UK Pricing, July 2026)
| Component | Deallocated | Active | Notes |
|---|---|---|---|
| Session Host VM (D2s_v5, moderate) | £0 | £90–120 | Largest cost driver |
| Session Host VM (B2s, light) | £0 | £30–40 | For lighter workloads |
| OS Disk (Standard SSD) | £2–3 | £2–3 | Always charged |
| Standard Public IP | £0 | £2–3 | Deleted when stopped |
| VNet/NSG/Services | £0 | £0 | Free (non-metered) |
| TOTAL (moderate, 1 VM) | £2–3 | £94–126 | 98% savings when deallocated |
Scaling Costs
| Configuration | Monthly Active | Monthly Deallocated | Use Case |
|---|---|---|---|
| 1× Moderate | £94–126 | £2–3 | Single occasional user |
| 2× Moderate | £188–252 | £4–6 | Two occasional users |
| 3× Light | £90–120 | £6–9 | Three light workload users |
| 5× Moderate (max) | £470–630 | £10–15 | Five dedicated users |
Cost Optimization Strategies
Strategy 1: Deallocate after use (Recommended)
- Deallocate VM to pause compute
- Preserves configuration and desktop state
- Quick resume (~2–3 min)
- Best for: Daily/weekly occasional use
Strategy 2: Delete and redeploy (for infrequent use)
- Complete infrastructure removal
- True zero cost between uses
- Longer recovery (~15–20 min)
- Best for: Monthly or less frequent use
Strategy 3: Light workload
- Switch from moderate (D2s_v5, £100–120/mo) to light (B2s, £35/mo)
- Requires 2–4 vCPU? Consider light
- Less RAM (4GB vs 8GB)
- Best for: Development, light document work
Related Documentation
- Quick Start – Get running in 5 minutes
- Prerequisites – Tools and environment setup
- Architecture Overview – How components interact
- Troubleshooting – Common issues and solutions
- Scripts Reference – PowerShell commands explained
Last Updated: July 2026