A Windows Desktop in the Cloud for About Lunch Money: Building an Azure Virtual Desktop Lab with Bicep

Hi there! It's BOB.

I've always wanted a second house. Somewhere I could pop in, use the computer, and pop back out. Real estate is expensive, though, so I got the next best thing: a Windows 11 desktop in Azure that I can open from my Mac, my tablet, or a web browser.

It's called Azure Virtual Desktop, and today we're building a small lab version of it. All of it is written as code, so you can build it, knock it down, and build it again in about 15 minutes.

And because I'm a sensible fellow who reads his credit card statement, we'll spend a good part of this article on keeping the bill small. Rover helped with that part. He's very good at finding things that were left running.

Let's go!

So… What Is Azure Virtual Desktop?

Azure Virtual Desktop (AVD) runs Windows desktops on virtual machines in Azure. You connect to them from almost anything: Windows, macOS, iPhone, Android, or a browser.

A few words you'll see a lot:

  • Session host: the VM people actually sign in to.
  • Host pool: a group of identical session hosts. Pooled means several people share each host. Personal means everyone gets their own VM.
  • Application group: what people get. Ours publishes one full desktop.
  • Workspace: the thing users subscribe to in the app. It's the "folder" their desktop shows up in.

Here's the nice part: the AVD control plane (host pools, app groups, workspaces, and the service that brokers connections) is free. You pay for the VMs, their disks, and any logs you keep. Turn the VM off, and most of the bill goes to sleep too.

💡 BOB's tip: AVD hosts use reverse connect. The VM calls out to the AVD service on port 443, so it needs no public IP address and no open inbound ports. That's one less door for anybody to knock on.

What the Lab Builds (and Why)

The whole lab is written in Bicep, Microsoft's language for describing Azure resources as code. You say what you want, and Azure works out how to get there. Run it again and it updates what's there, instead of making copies.

Here's what goes into one resource group, rg-avd-lab:

Piece What it is Why it's there
Network A virtual network, one subnet and a network security group with no inbound rules Reverse connect means nothing needs to come in.
Host pool Pooled, depth-first, up to 4 sessions per host, single sign-on turned on Depth-first fills one host before starting another, which is cheaper.
App group and workspace One desktop, shown as AVD lab Your users' group gets the Desktop Virtualization User role, so they can see it.
Session host One Windows 11 Enterprise multi-session + Microsoft 365 Apps VM with Trusted Launch Multi-session lets several people share one VM. Trusted Launch adds Secure Boot and a virtual TPM for free.
Entra join The VM joins Microsoft Entra ID directly, with optional Intune enrollment No domain controller to build, patch, or pay for.
Role assignments Virtual Machine User Login for users, Virtual Machine Administrator Login for admins Entra-joined VMs check these roles when you sign in.
Log Analytics Connection and error logs from the host pool, kept 30 days, with a daily cap So you can see what went wrong, without a surprise bill.
Scaling plan Deallocates the host whenever nobody is signed in The single biggest money saver. More below.
Auto-shutdown Stops the VM at 19:00 every day A backup, in case anything keeps it awake.
Start VM on Connect Starts a stopped host when someone connects So you never have to start it by hand.

The VM registers itself with the host pool through a small run command script that downloads and installs the AVD agent. Nobody has to RDP in and click "Next" a bunch of times. (I like clicking "Next". But not that much.)

💡 BOB's tip: Keep your own values (group IDs, VM size and so on) in a separate parameter file, such as lab.local.bicepparam, and tell Git to ignore *.local.bicepparam. Your template stays shareable, and your IDs never end up in a commit.

Before You Start: The Checklist

  • An Azure subscription, in the same Entra tenant the VMs will join. You'll need Owner on the resource group (or Contributor plus User Access Administrator), because the template creates role assignments.
  • An Entra group for your desktop users, such as AVD Users. Every member needs a license that includes AVD rights, such as Microsoft 365 E3/E5/Business Premium or Windows Enterprise E3/E5.
  • Intune (optional). If you want the host enrolled, you'll need an Intune license and MDM user scope set in automatic enrollment. Otherwise switch enrollment off in the parameters.
  • VM quota. Brand-new pay-as-you-go subscriptions often start with 0 cores for some VM families. Check before you deploy.
  • A Mac (or any computer) with the Azure CLI and Bicep.

⚠️ Heads up: Check your quota first. Our first try failed with QuotaExceeded because the subscription had zero cores for the Dsv5 family. The lab now defaults to Standard_D2s_v4, which costs the same and is more likely to have quota. Run az vm list-usage -l westus3 -o table, or in the portal search for Quotas > Compute.

One-time setup for single sign-on

Here's one that surprised me. If your tenant requires MFA (and it should! Security Defaults turns it on), the classic Windows password prompt on the VM can't do MFA. You end up typing your password over and over, like a doorbell that never rings.

The fix is single sign-on: you sign in once in Windows App with Entra ID, MFA and all, and Windows on the VM trusts it. The host pool turns this on with the RDP property enablerdsaadauth:i:1. But first, the tenant has to allow Microsoft Entra authentication for RDP. There's no portal switch for it, only Microsoft Graph. In PowerShell with the Microsoft Graph modules installed:

Connect-MgGraph -TenantId contoso.onmicrosoft.com -Scopes "Application-RemoteDesktopConfig.ReadWrite.All" -NoWelcome

# Microsoft Remote Desktop, and Windows Cloud Login
foreach ($appId in 'a4a365df-50f1-4397-bc59-1a1564b8bb9c', '270efc09-cd0d-444b-a71f-39af4910ec45') {
    $sp = Get-MgServicePrincipal -Filter "appId eq '$appId'"
    Update-MgServicePrincipalRemoteDesktopSecurityConfiguration -ServicePrincipalId $sp.Id -IsRemoteDesktopProtocolEnabled
    Get-MgServicePrincipalRemoteDesktopSecurityConfiguration -ServicePrincipalId $sp.Id | Select-Object IsRemoteDesktopProtocolEnabled
}

Both should say True. You only do this once per tenant.

Deploying from a Mac

1. Install the tools

brew update && brew install azure-cli   # the Azure CLI
az bicep install                        # Bicep, managed by the CLI
az login                                # opens a browser to sign in
az account set --subscription "Contoso Lab"

2. Collect a few IDs

The template needs the object ID of your users' group, and, for Start VM on Connect and the scaling plan, the object ID of the Azure Virtual Desktop service principal in your tenant:

az ad group show -g "AVD Users" --query id -o tsv
az ad sp show --id 9cdead84-a844-4324-93f2-b2e6bb768d07 --query id -o tsv

That long 9cdead84-… number is the same in every tenant. It's AVD's app ID. The object ID you get back is yours alone.

3. Fill in the parameter file

Here's roughly what a lab parameter file looks like. The cost settings are right there where you can see them:

using 'main.bicep'

param prefix = 'contoso-avd'
param userGroupObjectId = '<AVD Users object ID>'
param sessionHostCount = 1
param vmSize = 'Standard_D2s_v4'     // 2 vCPU, 8 GiB
param osDiskType = 'Standard_LRS'    // Standard HDD, the cheapest disk
param logDailyCapGb = '0.1'          // Log Analytics daily cap
param enrollInIntune = true
param shutdownTime = '1900'
param enableSingleSignOn = true
param enableScalingPlan = true
param avdServicePrincipalObjectId = '<AVD service principal object ID>'

// Read from the environment, so it never lands in a file
param localAdminPassword = readEnvironmentVariable('AVD_LOCAL_ADMIN_PASSWORD')

4. Create the resource group and set the password

az group create -n rg-avd-lab -l westus3
read -rs AVD_LOCAL_ADMIN_PASSWORD && export AVD_LOCAL_ADMIN_PASSWORD   # typed, not shown, not saved

💡 BOB's tip: The VM still gets a local administrator account, even though you'll sign in with Entra ID. Make the password long, keep it in your password manager, and don't put it in the parameter file.

5. Look before you leap: what-if

What-if shows what a deployment would create, change or delete, without doing any of it. It's like reading the recipe before you start cooking.

az deployment group what-if -g rg-avd-lab -p lab.local.bicepparam

You'll see a list of green + Create lines. On a later run you'll see ~ Modify or = NoChange instead. Read it. If something says - Delete and you didn't expect it, stop and find out why.

6. Deploy

az deployment group create -g rg-avd-lab -p lab.local.bicepparam

Go make a sandwich. It takes about 15 minutes. 🥪

When it finishes, give the host another 5 to 10 minutes to register, then check it in the portal: Azure Virtual Desktop > Host pools > your pool > Session hosts. You want Status: Available.

⚠️ Heads up: new subnets are private now. On our first deploy, the VM couldn't download the AVD agent, and registration failed with "Unable to connect to the remote server." Newer Azure network API versions create subnets without default outbound internet access. The lab's template now turns default outbound access on for its subnet, so the VM can reach the internet without a NAT gateway (which costs about $33 a month). For production, Microsoft recommends a NAT gateway instead, and default outbound access is being retired.

⚠️ Heads up: the registration token expires. The token that lets the VM join the host pool is good for 4 hours. If registration fails and you come back to it the next day, just run the deployment again. It makes a fresh token and tries again.

Connecting with Windows App

Windows App is Microsoft's client for AVD (and Windows 365, and Dev Box). It replaces the old Remote Desktop app.

  1. Install Windows App from the Mac App Store (or the store on your phone or tablet). No app? Use the web client at https://windows.cloud.microsoft.
  2. Add your work or school account, for example bob@contoso.com.
  3. Under AVD lab, open Desktop.
  4. Sign in with Entra ID (MFA included), then accept the Allow remote desktop connection? prompt. Tick Don't ask me again for this computer.

If the VM was stopped, Start VM on Connect wakes it up. The first connection takes a few minutes. Get a cup of tea. ☕

💡 BOB's tip: Your Windows 11 desktop comes with Microsoft 365 Apps already installed, so Word, Excel and Outlook are ready when you are.

Keeping Costs Low (Rover's Section)

Woof! It's Rover. BOB asked me to sniff out where the money goes. Here's what I found.

These are pay-as-you-go retail prices in the West US 3 region, in US dollars, as of November 2026. Your region and prices may differ.

First, the starting point: one Standard_D2s_v5 VM with a Standard SSD disk, before any of my tricks:

How you use it Per month
Running 24/7 about $80
Used every day, auto-shutdown at 19:00 about $39
Typical lab use, about 18 hours a week about $17
Stopped (deallocated) the whole month about $9.60 (the disk still bills)
Resource group deleted $0

And here's what to do about it.

1. Let the scaling plan put the VM to bed

Auto-shutdown only runs once a day. If you connect at 8 PM and close your laptop, the VM runs until 7 PM tomorrow. That's almost a whole day of paying for nothing.

A scaling plan is smarter, and it's free. Ours deallocates the host whenever nobody is signed in, at any time of day. Start VM on Connect starts it again when you come back. With both on, you only pay for the hours you actually use. The AVD service needs the Desktop Virtualization Power On Off Contributor role to do this, and the template assigns it.

It isn't instant. After the last person signs out, give it 15 to 30 minutes. The scaling plan checks in now and then, and waits a little before stopping a host. In our lab it took about half an hour, and then the VM showed Stopped (deallocated) all by itself. No Rover required.

⚠️ Heads up: sign out, don't just close the window. If you close Windows App, your session is only disconnected. It's still there, so the scaling plan thinks you're still around and leaves the VM running. Use Start > your name > Sign out.

And don't use Shut down inside Windows either! That stops Windows, but the VM stays allocated in Azure, and compute keeps billing. In the portal, Stopped costs money. Stopped (deallocated) doesn't.

2. Put a cap on Log Analytics

Log Analytics charges by the gigabyte you send it, after the first 5 GB a month, which are free. One small host pool sends well under that. But a chatty month at a 1 GB/day cap could cost about $58. The lab sets the cap to 0.1 GB a day, which is plenty, and keeps logs for 30 days, which is free.

3. Use a Standard HDD disk

The disk bills all the time, even while the VM is deallocated. Switching from a Standard SSD (about $9.60 a month) to a Standard HDD (about $5.90 a month) saves around $3.70 every month, whether you use the lab or not. Startup and sign-in are a little slower. For a lab, I think that's a fine trade. (I'm a dog. I'm patient.)

4. Pick a sensible VM size

Stick to 2 vCPUs and 8 GiB of memory. Sizes with 4 GiB are too small for Windows 11 with Microsoft 365 Apps, and you'll spend your time watching spinning circles. Compare a few sizes you have quota for:

  • Standard_D2s_v4 and Standard_D2s_v5 cost about the same ($0.096 an hour with the AVD license benefit). Use whichever one you have quota for.
  • B-series sizes, such as B2as_v2, are a bit cheaper, but they slow down if you keep the CPU busy for a long time.

Keep it to one host. Each extra host adds its own running hours and its own disk.

5. Delete the resource group when you're done for a while

Not going to use the lab for a few weeks? Delete the whole thing. It takes one command, and rebuilding it takes 15 minutes:

az group delete -n rg-avd-lab

⚠️ Heads up: Deleting the resource group doesn't remove the VM's device in Entra ID (or Intune). Delete it by hand under Entra admin center > Devices > All devices, or a rebuilt VM with the same name may have trouble joining.

What all that adds up to

With the lab's defaults (Standard_D2s_v4, Standard HDD, scaling plan on):

How you use it Per month
Running 24/7 about $76
About 18 hours a week about $13.40
Deallocated all month about $5.90
Resource group deleted $0

That's a Windows desktop in the cloud for about the price of a couple of lunches a month. Good boy, budget. 🐕

Things BOB Wishes Someone Had Told Him

⚠️ Assigned a role by hand? The template will trip over it. If you gave someone a role in the portal and the template tries to assign the same one, the deployment fails with RoleAssignmentExists. Remove the manual one and deploy again.

⚠️ Region support. The AVD objects (host pool, app group, workspace) store their metadata only in certain regions. If what-if says Microsoft.DesktopVirtualization isn't available where you are, keep the VMs in your region and put the AVD objects in a supported one, such as West US 2.

💡 Check the image name now and then. Microsoft retires old Windows 11 image SKUs. az vm image list-skus -l westus3 -p MicrosoftWindowsDesktop -f office-365 -o table lists what's current.

💡 Where to look when it misbehaves. The registration log on the VM is in C:\Windows\Temp, and the Log Analytics tables WVDConnections, WVDErrors and WVDCheckpoints tell you what happened during each connection.

Wrapping Up

So there you have it:

  • Azure Virtual Desktop gives you a Windows 11 desktop in Azure that you can reach from almost anything. The control plane is free.
  • Bicep describes the whole lab as code: network, host pool, Entra-joined VM, roles, logs and a scaling plan.
  • Check your quota, turn on Entra RDP sign-in once for single sign-on, then what-if and deploy from your Mac.
  • Connect with Windows App, and sign out when you're done.
  • A scaling plan, a log cap, a Standard HDD disk, a sensible VM size, and deleting the resource group when you're away keep the bill down to lunch money.

Next time, we'll look at keeping user profiles on Azure Files with FSLogix, so you can add a second host without anyone losing their desktop wallpaper.

See you next time!

— BOB (and Rover 🐕)

Sources

Author

jrainey

Just another HTMLy user