Table of Contents
So this is the 2nd part of a multi part series on my journey with Bicep and ARM Templates and deploying AVD as a PAW. Part One was the high level overview of what I have done.
- Part 1 – A high level overview
- Part 2 – The Host pool creation
- Part 3 – The Session Host creation
- Part 4 – Completing the setup
- Part 6 – Securing Access
- Part 5 – Logging on to AVD
Introduction
Welcome to the 2nd installment of my posts on using AVD as a PAW. It is probably clear from the previous post about which bit you need to do first, but if you’re still unsure then this is where you will need to start. Creating the core components, which include the Host pool, the Application Group and the Workspace.
So what are these? Lets have a look:
Host pool
A host pool is a collection of Azure virtual machines that are registered to Azure Virtual Desktop as session hosts. These VMs provide the computing resources for users to connect to. Host pools can be configured as either personal (dedicated to individual users) or pooled (shared among multiple users) to optimize resource usage and cost
In this scenario I am configuring them as pooled
Application Group
An application group is a logical grouping of applications that are available on session hosts within a host pool. It controls whether users have access to a full desktop or specific applications. There are two types of application groups: Desktop (provides access to a full Windows desktop) and RemoteApp (provides access to individual applications)
I am configuring the cPAW as a Desktop.
Workspace
A workspace is a logical grouping of application groups. It serves as the user-facing interface where users can access their assigned desktops and applications. Workspaces help organize and manage the resources available to users, making it easier for them to find and launch the applications they need.
This is what is presented to the user in the Windows App:

More information on these can be found here:
Azure Virtual Desktop terminology – Azure | Microsoft Learn
Preferred application group type behavior for pooled host pools in Azure Virtual Desktop | Microsoft Learn
Creating the core components
So, you can create the core core components via the GUI or via an ARM Template. I have decided that doing it via the ARM Template seems to be the best way to go, and is a lot quicker.
The Bicep Script
My script can be found here: CloudPAW/cPAW-HostpoolCreation.bicep at main · andrew-kemp/CloudPAW
There is not much to it when you break it down, 4 sections:
- First – Parameters
- Second – Create the Host Pool
- Third – Create the Application Group
- Fourth – Create the Workspace
param location string = 'uksouth'
param sessionHostPrefix string = 'cPAW'
//Deploy the Hostpool
resource HostPool 'Microsoft.DesktopVirtualization/hostPools@2021-07-12' = {
name: '${sessionHostPrefix}-HostPool'
location: location
properties: {
friendlyName: '${sessionHostPrefix} Host Pool'
description: '${sessionHostPrefix} Virual Privileged Access Workstaiotn Host Pool for privielged users to securely access the Microsoft Admin centers from'
hostPoolType: 'Pooled'
loadBalancerType: 'BreadthFirst'
maxSessionLimit: 5
personalDesktopAssignmentType: 'Automatic'
startVMOnConnect: true
preferredAppGroupType: 'Desktop'
customRdpProperty: 'enablecredsspsupport:i:0;authentication level:i:2;enablerdsaadauth:i:1;'
}
}
//Deploy the vPAW Desktop Application Group
resource AppGroup 'Microsoft.DesktopVirtualization/applicationGroups@2021-07-12' = {
name: '${sessionHostPrefix}-AppGroup'
location: location
properties: {
description: '${sessionHostPrefix} Application Group'
friendlyName: '${sessionHostPrefix} Desktop Application Group'
hostPoolArmPath: HostPool.id
applicationGroupType: 'Desktop'
}
}
//Deploy the vPAW Workspace
resource Workspace 'Microsoft.DesktopVirtualization/workspaces@2021-07-12' = {
name: '${sessionHostPrefix}-Workspace'
location: location
properties: {
description: '${sessionHostPrefix} Workspace for Privileged Users'
friendlyName: '${sessionHostPrefix} Workspace'
applicationGroupReferences: [
AppGroup.id
]
}
}
I decided to keep the naming of the resources simple, so I went with cPAW, Cloud Privileged Access Workstation, so I defined that in the parameter of the script.
Converting to JSON
In the previous post I showed you the way I covert this to JSON is by using Visual Studio Code. But to save you going back to that post, right click on the tab of the open Bicep file and then click Build ARM Template:

Visual Studio Code then creates a JSON file for you in the same location where the Bicep file is saved. When converted to JSON for the ARM Template it looks like this, I think you’ll agree thats not as easy to read as the Bicep, and there for easy to update/edit:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.33.93.31351",
"templateHash": "1209455326646934856"
}
},
"parameters": {
"location": {
"type": "string",
"defaultValue": "uksouth"
},
"sessionHostPrefix": {
"type": "string",
"defaultValue": "cPAW"
},
"numberOfHosts": {
"type": "int",
"defaultValue": 2
},
"adminPassword": {
"type": "securestring"
},
"adminUsername": {
"type": "string",
"defaultValue": "cPAW-Admin"
},
"hostPoolRegistrationInfoToken": {
"type": "string",
"defaultValue": "Enter HostPool Registration Key here"
}
},
"variables": {
"modulesURL": "[format('https://wvdportalstorageblob.blob.{0}/galleryartifacts/Configuration_1.0.02797.442.zip', environment().suffixes.storage)]"
},
"resources": [
{
"type": "Microsoft.Network/virtualNetworks",
"apiVersion": "2021-05-01",
"name": "[format('{0}-vNet', parameters('sessionHostPrefix'))]",
"location": "[parameters('location')]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"192.168.250.0/24"
]
},
"subnets": [
{
"name": "[format('{0}-Subnet', parameters('sessionHostPrefix'))]",
"properties": {
"addressPrefix": "192.168.250.0/24"
}
}
]
}
},
{
"copy": {
"name": "nic",
"count": "[length(range(0, parameters('numberOfHosts')))]"
},
"type": "Microsoft.Network/networkInterfaces",
"apiVersion": "2020-11-01",
"name": "[format('{0}-{1}-nic', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[copyIndex()])]",
"location": "[parameters('location')]",
"properties": {
"ipConfigurations": [
{
"name": "name",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[reference(resourceId('Microsoft.Network/virtualNetworks', format('{0}-vNet', parameters('sessionHostPrefix'))), '2021-05-01').subnets[0].id]"
}
}
}
]
},
"dependsOn": [
"[resourceId('Microsoft.Network/virtualNetworks', format('{0}-vNet', parameters('sessionHostPrefix')))]"
]
},
{
"copy": {
"name": "VM",
"count": "[length(range(0, parameters('numberOfHosts')))]"
},
"type": "Microsoft.Compute/virtualMachines",
"apiVersion": "2020-12-01",
"name": "[format('{0}-{1}', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[copyIndex()])]",
"location": "[parameters('location')]",
"identity": {
"type": "SystemAssigned"
},
"properties": {
"hardwareProfile": {
"vmSize": "Standard_B4ms"
},
"osProfile": {
"computerName": "[format('{0}-{1}', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[copyIndex()])]",
"adminUsername": "[parameters('adminUsername')]",
"adminPassword": "[parameters('adminPassword')]"
},
"storageProfile": {
"imageReference": {
"publisher": "MicrosoftWindowsDesktop",
"offer": "Windows-11",
"sku": "win11-24h2-avd",
"version": "latest"
}
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', format('{0}-{1}-nic', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[range(0, parameters('numberOfHosts'))[copyIndex()]]))]"
}
]
}
},
"dependsOn": [
"[resourceId('Microsoft.Network/networkInterfaces', format('{0}-{1}-nic', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[range(0, parameters('numberOfHosts'))[copyIndex()]]))]"
]
},
{
"copy": {
"name": "entraIdJoin",
"count": "[length(range(0, parameters('numberOfHosts')))]"
},
"type": "Microsoft.Compute/virtualMachines/extensions",
"apiVersion": "2021-11-01",
"name": "[format('{0}/{1}', format('{0}-{1}', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[range(0, parameters('numberOfHosts'))[copyIndex()]]), format('{0}-{1}-EntraJoinEntrollIntune', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[copyIndex()]))]",
"location": "[parameters('location')]",
"properties": {
"publisher": "Microsoft.Azure.ActiveDirectory",
"type": "AADLoginForWindows",
"typeHandlerVersion": "2.2",
"autoUpgradeMinorVersion": true,
"enableAutomaticUpgrade": false,
"settings": {
"mdmId": "0000000a-0000-0000-c000-000000000000"
}
},
"dependsOn": [
"[resourceId('Microsoft.Compute/virtualMachines', format('{0}-{1}', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[range(0, parameters('numberOfHosts'))[copyIndex()]]))]"
]
},
{
"copy": {
"name": "guestAttestationExtension",
"count": "[length(range(0, parameters('numberOfHosts')))]"
},
"type": "Microsoft.Compute/virtualMachines/extensions",
"apiVersion": "2024-07-01",
"name": "[format('{0}/{1}', format('{0}-{1}', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[range(0, parameters('numberOfHosts'))[copyIndex()]]), format('{0}-{1}-guestAttestationExtension', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[copyIndex()]))]",
"location": "[parameters('location')]",
"properties": {
"publisher": "Microsoft.Azure.Security.WindowsAttestation",
"type": "GuestAttestation",
"typeHandlerVersion": "1.0",
"autoUpgradeMinorVersion": true
},
"dependsOn": [
"entraIdJoin",
"[resourceId('Microsoft.Compute/virtualMachines', format('{0}-{1}', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[range(0, parameters('numberOfHosts'))[copyIndex()]]))]"
]
},
{
"copy": {
"name": "SessionPrep",
"count": "[length(range(0, parameters('numberOfHosts')))]"
},
"type": "Microsoft.Compute/virtualMachines/extensions",
"apiVersion": "2021-03-01",
"name": "[format('{0}/{1}', format('{0}-{1}', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[range(0, parameters('numberOfHosts'))[copyIndex()]]), format('{0}-{1}-CSessionPrep', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[copyIndex()]))]",
"location": "[parameters('location')]",
"properties": {
"publisher": "Microsoft.Compute",
"type": "CustomScriptExtension",
"typeHandlerVersion": "1.10",
"autoUpgradeMinorVersion": true,
"settings": {
"fileUris": [
"https://raw.githubusercontent.com/andrew-kemp/CloudPAW/refs/heads/main/SessionHostPrep.ps1"
],
"commandToExecute": "powershell -ExecutionPolicy Unrestricted -File SessionHostPrep.ps1"
}
},
"dependsOn": [
"guestAttestationExtension",
"[resourceId('Microsoft.Compute/virtualMachines', format('{0}-{1}', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[range(0, parameters('numberOfHosts'))[copyIndex()]]))]"
]
},
{
"copy": {
"name": "dcs",
"count": "[length(range(0, parameters('numberOfHosts')))]"
},
"type": "Microsoft.Compute/virtualMachines/extensions",
"apiVersion": "2024-03-01",
"name": "[format('{0}/{1}', format('{0}-{1}', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[range(0, parameters('numberOfHosts'))[copyIndex()]]), format('{0}-{1}-JointoHostPool', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[copyIndex()]))]",
"location": "[parameters('location')]",
"properties": {
"publisher": "Microsoft.Powershell",
"type": "DSC",
"typeHandlerVersion": "2.76",
"settings": {
"modulesUrl": "[variables('modulesURL')]",
"configurationFunction": "Configuration.ps1\\AddSessionHost",
"properties": {
"hostPoolName": "[format('{0}-HostPool', parameters('sessionHostPrefix'))]",
"aadJoin": true
}
},
"protectedSettings": {
"properties": {
"registrationInfoToken": "[parameters('hostPoolRegistrationInfoToken')]"
}
}
},
"dependsOn": [
"SessionPrep",
"[resourceId('Microsoft.Compute/virtualMachines', format('{0}-{1}', parameters('sessionHostPrefix'), range(0, parameters('numberOfHosts'))[range(0, parameters('numberOfHosts'))[copyIndex()]]))]"
]
}
]
}
Now you have the ARM Template you need to deploy.
Deploying the custom template
Just a wee note, when it then comes to deploying the custom template in Azure you can update the prefix as you see fit.
I know I put these steps in the previous post and maybe repeating myself here. But, to deploy the custom template all you need to do is go to the search bar in Azure and type deploy and then in the top of the list you should see Deploy a custom template. You might also have it listed in your Azure services row:

Click on Build your own template in the editor:

You’ll then see the following:

Delete the content of the template:

Then paste the JSON ARM Template in the editor:

You’ll then see that there are 2 parameters set and 3 resources to create. Click on Save
Select the Subscription that you want to deploy the template to, and then set the resource group, (again I am using cPAW for this as that is was is then used through the rest of the series, I will look at setting this to be something else so you can completely customise it later).
And set the Session Host Prefix, although we are not creating the session hosts in this post, I wanted to use the same prefix for the core component names:

Click on Review + create:
Then click on Create

This should take no more than a few seconds to complete and you will then have 3 new resources created:

If you then go to the resource group you will see the following:

And, if you type Azure Virtual in to the search bar and select Azure Virtual Desktop you will then see the new resources listed in there:




This is where you can then update the settings for each of these like the friendly names etc… But, I have got a script that I’ll go into later on in another part of this series to do all that for us.
Conculsion
So this is the core components of AVD Deployed for the Cloud PAW. I am sure there are other ways to do it, and, yes I know that you can also run the commands from within Visual Studio Code as well. But hopefully from here you’ll see how the Bicep script is setup and then how then the JSON template is then used from that Bicep file.
With the settings I opted for in the Bicep file my Host pool has the following settings:
Users sign in with Microsoft Entra single sign-on:

And The Start VM on connect is enabled, this will then mean once I have configured the session hosts to shutdown at 19:00:00 each day they will then start up when the next user connects.
