Table of Contents

Use Azure AD DS and Window Virtual Desktop as a Test Environment

Summary: How to use Azure AD DS and Window Virtual Desktop as a Test Environment.
Date: Around 2020
Refactor: 20 February 2025: Checked links and formatting.

Note: This article was originally created in the first few months in 2020, before the Corona pandemic reached Europe and the US. Forced by the pandemic Microsoft made a lot of steps in this manual a lot more easy, and this article probably less relevant.

In this article the following services are being used:

Create a New Azure Subscription

As we want to test in completely separated and empty environment the best way forward is to create an new empty environment. Usually you could create a free account but I used one before and free accounts are limited to 1 per customer. So I decided to create a new subscription:

This might take a while, but afterwards you'll be the proud owner of an additional subscription.

Azure AD

The first step we need to do now is to create a new tenant and associate that with this subscription.

Note that a subscription can only trust one directory, but a directory can trust multiple subscriptions

Now that we've created the directory we have to associate it with the new subscription:

Now, in the subscriptions overview, to see the new subscription, use “Switch Directories” to see the new subscription. The documentation mentioned it could take hours for everything to show correctly, so make sure everything looks ok before you continue.

Setup Azure Active Directory Domain Services

For now we'll create a basic instance. This will create an instance with the default configuration settings for networking and synchronization.

Note that provisioning a domain can take up to more than an hour. To check the progress click on the resource group (RG_AzureADDS) and then on the Azure AD Domain Services object (aadtest001getsh.onmicrosoft.com) in the list of resources.

Template

You can also download a template for automation. This will provide two files:

Template.json
template.json
  1. {
  2. "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  3. "contentVersion": "1.0.0.0",
  4. "parameters": {
  5. "apiVersion": {
  6. "type": "string"
  7. },
  8. "sku": {
  9. "type": "string"
  10. },
  11. "domainConfigurationType": {
  12. "type": "string"
  13. },
  14. "domainName": {
  15. "type": "string"
  16. },
  17. "filteredSync": {
  18. "type": "string"
  19. },
  20. "location": {
  21. "type": "string"
  22. },
  23. "notificationSettings": {
  24. "type": "object"
  25. },
  26. "subnetName": {
  27. "type": "string"
  28. },
  29. "vnetName": {
  30. "type": "string"
  31. },
  32. "vnetAddressPrefixes": {
  33. "type": "array"
  34. },
  35. "subnetAddressPrefix": {
  36. "type": "string"
  37. },
  38. "nsgName": {
  39. "type": "string"
  40. }
  41. },
  42. "resources": [
  43. {
  44. "apiVersion": "2017-06-01",
  45. "type": "Microsoft.AAD/DomainServices",
  46. "name": "[parameters('domainName')]",
  47. "location": "[parameters('location')]",
  48. "dependsOn": [
  49. "[concat('Microsoft.Network/virtualNetworks/', parameters('vnetName'))]"
  50. ],
  51. "properties": {
  52. "domainName": "[parameters('domainName')]",
  53. "subnetId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Network/virtualNetworks/', parameters('vnetName'), '/subnets/', parameters('subnetName'))]",
  54. "filteredSync": "[parameters('filteredSync')]",
  55. "domainConfigurationType": "[parameters('domainConfigurationType')]",
  56. "notificationSettings": "[parameters('notificationSettings')]",
  57. "sku": "[parameters('sku')]"
  58. }
  59. },
  60. {
  61. "type": "Microsoft.Network/NetworkSecurityGroups",
  62. "name": "[parameters('nsgName')]",
  63. "location": "[parameters('location')]",
  64. "properties": {
  65. "securityRules": [
  66. {
  67. "name": "AllowSyncWithAzureAD",
  68. "properties": {
  69. "access": "Allow",
  70. "priority": 101,
  71. "direction": "Inbound",
  72. "protocol": "Tcp",
  73. "sourceAddressPrefix": "AzureActiveDirectoryDomainServices",
  74. "sourcePortRange": "*",
  75. "destinationAddressPrefix": "*",
  76. "destinationPortRange": "443"
  77. }
  78. },
  79. {
  80. "name": "AllowPSRemoting",
  81. "properties": {
  82. "access": "Allow",
  83. "priority": 301,
  84. "direction": "Inbound",
  85. "protocol": "Tcp",
  86. "sourceAddressPrefix": "AzureActiveDirectoryDomainServices",
  87. "sourcePortRange": "*",
  88. "destinationAddressPrefix": "*",
  89. "destinationPortRange": "5986"
  90. }
  91. },
  92. {
  93. "name": "AllowRD",
  94. "properties": {
  95. "access": "Allow",
  96. "priority": 201,
  97. "direction": "Inbound",
  98. "protocol": "Tcp",
  99. "sourceAddressPrefix": "CorpNetSaw",
  100. "sourcePortRange": "*",
  101. "destinationAddressPrefix": "*",
  102. "destinationPortRange": "3389"
  103. }
  104. }
  105. ]
  106. },
  107. "apiVersion": "2019-09-01"
  108. },
  109. {
  110. "type": "Microsoft.Network/virtualNetworks",
  111. "name": "[parameters('vnetName')]",
  112. "location": "[parameters('location')]",
  113. "apiVersion": "2019-09-01",
  114. "dependsOn": [
  115. "[concat('Microsoft.Network/NetworkSecurityGroups/', parameters('nsgName'))]"
  116. ],
  117. "properties": {
  118. "addressSpace": {
  119. "addressPrefixes": "[parameters('vnetAddressPrefixes')]"
  120. },
  121. "subnets": [
  122. {
  123. "name": "[parameters('subnetName')]",
  124. "properties": {
  125. "addressPrefix": "[parameters('subnetAddressPrefix')]",
  126. "networkSecurityGroup": {
  127. "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Network/NetworkSecurityGroups/', parameters('nsgName'))]"
  128. }
  129. }
  130. }
  131. ]
  132. }
  133. }
  134. ],
  135. "outputs": {}
  136. }


Parameters.json * Changed the email address under additionalRecipients

parameters.json
  1. {
  2. "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  3. "contentVersion": "1.0.0.0",
  4. "parameters": {
  5. "apiVersion": {
  6. "value": "2017-06-01"
  7. },
  8. "sku": {
  9. "value": "Standard"
  10. },
  11. "domainConfigurationType": {
  12. "value": "FullySynced"
  13. },
  14. "domainName": {
  15. "value": "aadtest001getsh.onmicrosoft.com"
  16. },
  17. "filteredSync": {
  18. "value": "Disabled"
  19. },
  20. "location": {
  21. "value": "westeurope"
  22. },
  23. "notificationSettings": {
  24. "value": {
  25. "notifyGlobalAdmins": "Enabled",
  26. "notifyDcAdmins": "Enabled",
  27. "additionalRecipients": [
  28. "test@test.com"
  29. ]
  30. }
  31. },
  32. "subnetName": {
  33. "value": "aadds-subnet"
  34. },
  35. "vnetName": {
  36. "value": "aadds-vnet"
  37. },
  38. "vnetAddressPrefixes": {
  39. "value": [
  40. "10.0.0.0/24"
  41. ]
  42. },
  43. "subnetAddressPrefix": {
  44. "value": "10.0.0.0/24"
  45. },
  46. "nsgName": {
  47. "value": "aadds-nsg"
  48. }
  49. }
  50. }

Update DNS

DNS is a key resource for an Active Directory Domain, and you need to configure the virtual network with the new DNS settings.

Enable User Accounts for Azure AD Domain Services

In this setup, we only have one user so far. Usually you would have users change their passwords before they can use Azure AD Domain Services. We will now create users so we can use Azure AD Domain Services.

Repeat the steps above for:

Use Cloud Shell to setup a permanent password:

$pass = ConvertTo-SecureString -String "NewPass123!!" -AsPlainText -Force
Connect-AzureAD
Set-AzureADUserPassword -ObjectId "4d6ea095-ec2b-491f-b785-ee68f32661be" -ForceChangePasswordNextLogin $false -Password $pass
 
#testuser:
Set-AzureADUserPassword -ObjectId "c4e719d6-d895-49d8-9d58-099c971f8770" -ForceChangePasswordNextLogin $false -Password $pass
 
#poweruser:
Set-AzureADUserPassword -ObjectId "0533f49d-5ffb-476c-8f8a-433b175f5be7" -ForceChangePasswordNextLogin $false -Password $pass

Configure Network Connectivity for Application Workload

Now that Azure AD Domain Services is correctly deployed it would actually be nice that we can manage it. During the deployment we configured the default settings, which created a separate virtual network and subnet for the Azure AD Domain Services. It is recommended to use this subnet only for the Azure AD Domain Services, which means we can create additional subnets or create additional networks and peer the networks. We will create a virtual subnet:

Create and Configure a Management Server

Now that we've created a management network we can create a management server and join the server to the domain.

This might take a few minutes, once the deployment is done you can click “Setup auto-shutdown” to configure automatic shutdown of the VM, in case you might ever forget:

Connect To Management VM using Azure Bastion

To connect to the Management VM we'll use Azure Bastion, which you could see as a gateway or stepping stone server between the internet and your private network in Azure. Do note that this is https://azure.microsoft.com/en-us/pricing/details/azure-bastion/

Once the Bastion Host is created it is possible to connect to the VM using only your browser.

Join the VM to the Azure AD DS Managed Domain

Once you've logged in to the VM, you can start to join it to the domain and install management tools

Now the Management VM will reboot, and afterwards you can login using Bastion and the sjoerdadmin credentials. We will now install the Remote Server Administration Tools.

Now you can use the Active Directory, DNS and Group Policy Management administration tools from the Server Manager → Tools section.

Note that before you can manage DNS you need to connect to a DNS server. You can fill in the domain name (aadtest001getsh.onmicrosoft.com) to automatically connect to one.

Create Windows Virtual Desktop Tenant

Now that we have a working Active Directory environment in the cloud we also want to provide applications to users. We will use Windows Virtual Desktop for that. This is, right now, quite a new service, and a lot of individual steps are required. We start with creating a tenant in Windows Virtual Desktop. We need a Global administrator for this, but we can't use the original global administrator because that one is part of a different directory. This shows in Azure Active Directory which shows the account as from an External Azure Active Directory.

Now open a browser as the specified user. To do so, login to the Management VM, and start by disabling “IE Enhanced Security Configuration” for administrators under the Local Server properties in Server Manager. Then start Internet Explorer.

Now that we've performed all required steps we can actually create a Windows Virtual Desktop client. Unfortunately that is done with a legacy powershell module, which means we can't use the previously configured cloud shell. Windows Server 2019 still comes with PowerShell 5.1 so we will do these steps from the Management VM. Start PowerShell as administrator as we will first install the required module.

Install-Module -Name Microsoft.RDInfra.RDPowerShell
Import-Module -Name Microsoft.RDInfra.RDPowerShell
Add-RdsAccount -DeploymentUrl "https://rdbroker.wvd.microsoft.com"
# use the sjoerdadmin@aadtest001getshifting.onmicrosoft.com account to log in
New-RdsTenant -Name virtualdesktoptest001 -AadTenantId 569b3e72-89ce-4115-9858-ce53d8e5c490 -AzureSubscriptionId c18b1986-f82f-4809-838a-39cfe062f21b
# Optionally you could assign administrative access to a second user
New-RdsRoleAssignment -TenantName <TenantName> -SignInName <Upn> -RoleDefinitionName "RDS Owner"
Note that once installed, you can perform the succeeding steps in a non-administrative powershell session

Windows Virtual Desktop Service Principals

Service principals are identities that you can create in Azure Active Directory to assign roles and permissions for a specific purpose. In Windows Virtual Desktop, you can create a service principal to “Automate specific Windows Virtual Desktop management tasks” and “Use as credentials in place of MFA-required users when running any Azure Resource Manager template for Windows Virtual Desktop”.

We start by installing the AzureAD powershell module in an administrative powershell session

Install-Module AzureAD

Then run the following commands:

import-module Microsoft.RDInfra.RDPowershell
import-module AzureAD
$aadContext = Connect-AzureAD
# use the sjoerdadmin@aadtest001getshifting.onmicrosoft.com account to log in
$svcPrincipal = New-AzureADApplication -AvailableToOtherTenants $true -DisplayName "Windows Virtual Desktop Svc Principal"
$svcPrincipalCreds = New-AzureADApplicationPasswordCredential -ObjectId $svcPrincipal.ObjectId
# Now you need three essential pieces of information
# The Password
$svcPrincipalCreds.Value
 
R0SeCl0PevMnhbyHBcvinlTuc9WHlsSNL7E2iqfHtSU=
 
# Tenant ID
$aadContext.TenantId.Guid
569b3e72-89ce-4115-9858-ce53d8e5c490
# Application ID
$svcPrincipal.AppId
1bbce553-9951-41a8-b527-b6a0d2d30479
# Now we create the role assignments
Add-RdsAccount -DeploymentUrl "https://rdbroker.wvd.microsoft.com"
# use the sjoerdadmin@aadtest001getshifting.onmicrosoft.com account to log in
Get-RdsTenant
# This displayes the TenantName (among other information), please note it: virtualdesktoptest001
$myTenantName = "virtualdesktoptest001"
New-RdsRoleAssignment -RoleDefinitionName "RDS Owner" -ApplicationId $svcPrincipal.AppId -TenantName $myTenantName
$creds = New-Object System.Management.Automation.PSCredential($svcPrincipal.AppId, (ConvertTo-SecureString $svcPrincipalCreds.Value -AsPlainText -Force))
Add-RdsAccount -DeploymentUrl "https://rdbroker.wvd.microsoft.com" -Credential $creds -ServicePrincipal -AadTenantId $aadContext.TenantId.Guid

Create a Host Pool for Windows Virtual Desktop

Before we can create the hostpool we first need to disable MFA for the account that will perform the domain join. For new tenants, the security defaults now include MFA, which is not supported for accounts that are used for the domain join of Windows Virtual Desktop hosts.

Create a hostpool to host the VMs that will be acting as the Windows Virtual Desktop.

Note that if you want to add additional users to the desktop application group you need to do so using powershell:
Add-RdsAccount -DeploymentUrl "https://rdbroker.wvd.microsoft.com"
Add-RdsAppGroupUser <tenantname> <hostpoolname> "Desktop Application Group" -UserPrincipalName <userupn>

To remove users:

Get-RdsAppGroup Virtualdesktoptest001 HostPool_Desktop01
#Remove-RdsAppGroupUser -TenantName "contoso" -HostPoolName "contosoHostPool" -AppGroupName "officeApps" -UserPrincipalName "user1@contoso.com"
 
Remove-RdsAppGroupUser -TenantName Virtualdesktoptest001 -HostPoolName HostPool_Desktop01 -AppGroupName "Desktop Application Group" -UserPrincipalName "sjoerdpower@aadtest001getshifting.onmicrosoft.com"

Connect to the Desktop

There are several options to connect to the virtual desktop, but, some work better than others. You have the https://docs.microsoft.com/en-us/azure/virtual-desktop/connect-windows-7-and-10 but it requires a few firewall urls to be opened, which is not always convenient when doing tests. I found the best solution was to use the https://docs.microsoft.com/en-us/azure/virtual-desktop/connect-web, which involved to startup a private chrome broweser window, connect to https://rdweb.wvd.microsoft.com/webclient and login using the sjoerdtest@aadtest001getshifting.onmicrosoft.com account.

After logging in you first see the Virtual Desktop Tenant: Virtualdesktoptest001, and when clicking through you'll be presented with the HostPool: HostPool_Desktop01 which will connect you with the actual desktop. Note that you also need to logon to the desktop itself, again using the sjoerdtest account.

Publish a RemoteApp

Now that we have a working desktop I also want to be able to publish a single application. To do so, we will work again with the Windows Virtual Desktop PowerShell module which is installed on the management server

import-module Microsoft.RDInfra.RDPowershell
# Login with your sjoerdadmin account: sjoerdadmin@aadtest001getshifting.onmicrosoft.com
Add-RdsAccount -DeploymentUrl "https://rdbroker.wvd.microsoft.com"
# Create a new empty RemoteApp app group
# New-RdsAppGroup <tenantname> <hostpoolname> <appgroupname> -ResourceType "RemoteApp"
New-RdsAppGroup Virtualdesktoptest001 HostPool_Desktop01 WindowsApps -ResourceType "RemoteApp"
# Verify
# Get-RdsAppGroup <tenantname> <hostpoolname>
Get-RdsAppGroup Virtualdesktoptest001 HostPool_Desktop01
#  Get a list of Start menu apps on the host pool's virtual machine image. Write down the values for FilePath, IconPath, IconIndex and the AppAlias.
# Get-RdsStartMenuApp <tenantname> <hostpoolname> <appgroupname>
Get-RdsStartMenuApp Virtualdesktoptest001 HostPool_Desktop01 WindowsApps
<#
TenantGroupName      : Default Tenant Group
TenantName           : Virtualdesktoptest001
HostPoolName         : HostPool_Desktop01
AppGroupName         : WindowsApps
AppAlias             : snippingtool
FriendlyName         : Snipping Tool
FilePath             : C:\windows\system32\SnippingTool.exe
CommandLineArguments :
IconPath             : C:\windows\system32\SnippingTool.exe
IconIndex            : 0
 
TenantGroupName      : Default Tenant Group
TenantName           : Virtualdesktoptest001
HostPoolName         : HostPool_Desktop01
AppGroupName         : WindowsApps
AppAlias             : taskmanager
FriendlyName         : Task Manager
FilePath             : C:\windows\system32\taskmgr.exe
CommandLineArguments : /7
IconPath             : C:\windows\system32\Taskmgr.exe
IconIndex            : -30651
 
TenantGroupName      : Default Tenant Group
TenantName           : Virtualdesktoptest001
HostPoolName         : HostPool_Desktop01
AppGroupName         : WindowsApps
AppAlias             : wordpad
FriendlyName         : Wordpad
FilePath             : C:\Program Files\Windows NT\Accessories\wordpad.exe
CommandLineArguments :
IconPath             : C:\Program Files\Windows NT\Accessories\wordpad.exe
IconIndex            : 0
#>
#  Run the following cmdlet to install the application based on AppAlias
# New-RdsRemoteApp <tenantname> <hostpoolname> <appgroupname> -Name <remoteappname> -AppAlias <appalias>
New-RdsRemoteApp Virtualdesktoptest001 HostPool_Desktop01 WindowsApps -Name "Snipping Tool" -AppAlias snippingtool
New-RdsRemoteApp Virtualdesktoptest001 HostPool_Desktop01 WindowsApps -Name "Task Manager" -AppAlias taskmanager
New-RdsRemoteApp Virtualdesktoptest001 HostPool_Desktop01 WindowsApps -Name Wordpad -AppAlias wordpad
# Verify
# Get-RdsRemoteApp <tenantname> <hostpoolname> <appgroupname>
Get-RdsRemoteApp Virtualdesktoptest001 HostPool_Desktop01 WindowsApps
# Grant users access. Note that within a hostgroup you can't assign users to both desktop and remote app groups.
#Add-RdsAppGroupUser <tenantname> <hostpoolname> <appgroupname> -UserPrincipalName <userupn>
Add-RdsAppGroupUser Virtualdesktoptest001 HostPool_Desktop01 WindowsApps -UserPrincipalName "sjoerdpower@aadtest001getshifting.onmicrosoft.com"

Now you can user the poweruser to login through the https://rdweb.wvd.microsoft.com/webclient and access the published apps.

Add Custom App

You can also add an application which is not listed as publishable in the Get-RdsStartMenuApp command. Notepad is not listed, but you can add it like this:

#New-RdsRemoteApp <tenantname> <hostpoolname> <appgroupname> -Name <remoteappname> -Filepath <filepath>  -IconPath <iconpath> -IconIndex <iconindex>
New-RdsRemoteApp Virtualdesktoptest001 HostPool_Desktop01 WindowsApps -Name Notepad -Filepath "C:\WINDOWS\system32\notepad.exe"  -IconPath "C:\WINDOWS\system32\notepad.exe" -IconIndex 0

Create a Master Image for Line of Business Applications

We will now create a Master Image with a business application. For this test we will use VSCode with a few extensions.

Create the VM

Once the VM is created you can login using Bastion and the credentials provided during VM creation.

Prepare the VM and Applications as a Windows Virtual Desktop

Follow the steps as explained https://docs.microsoft.com/en-us/azure/virtual-desktop/set-up-customize-master-image and https://docs.microsoft.com/en-us/azure/virtual-desktop/install-office-on-wvd-master-image and https://docs.microsoft.com/en-us/azure/virtual-desktop/create-host-pools-user-profile#configure-the-fslogix-profile-container

<Configuration>
  <Add OfficeClientEdition="64" Channel="Monthly">
    <Product ID="O365ProPlusRetail">
      <Language ID="en-US" />
      <Language ID="MatchOS" />
      <ExcludeApp ID="Groove" />
      <ExcludeApp ID="Lync" />
      <ExcludeApp ID="OneDrive" />
      <ExcludeApp ID="Teams" />
    </Product>
  </Add>
  <RemoveMSI/>
  <Updates Enabled="FALSE"/>
  <Display Level="None" AcceptEULA="TRUE" />
  <Logging Level=" Standard" Path="%temp%\WVDOfficeInstall" />
  <Property Name="FORCEAPPSHUTDOWN" Value="TRUE"/>
  <Property Name="SharedComputerLicensing" Value="1"/>
</Configuration>
rem Mount the default user registry hive
reg load HKU\TempDefault C:\Users\Default\NTUSER.DAT
rem Must be executed with default registry hive mounted.
reg add HKU\TempDefault\SOFTWARE\Policies\Microsoft\office\16.0\common /v InsiderSlabBehavior /t REG_DWORD /d 2 /f
rem Set Outlook's Cached Exchange Mode behavior
rem Must be executed with default registry hive mounted.
reg add "HKU\TempDefault\software\policies\microsoft\office\16.0\outlook\cached mode" /v enable /t REG_DWORD /d 1 /f
reg add "HKU\TempDefault\software\policies\microsoft\office\16.0\outlook\cached mode" /v syncwindowsetting /t REG_DWORD /d 1 /f
reg add "HKU\TempDefault\software\policies\microsoft\office\16.0\outlook\cached mode" /v CalendarSyncWindowSetting /t REG_DWORD /d 1 /f
reg add "HKU\TempDefault\software\policies\microsoft\office\16.0\outlook\cached mode" /v CalendarSyncWindowSettingMonths  /t REG_DWORD /d 1 /f
rem Unmount the default user registry hive
reg unload HKU\TempDefault

rem Set the Office Update UI behavior.
reg add HKLM\SOFTWARE\Policies\Microsoft\office\16.0\common\officeupdate /v hideupdatenotifications /t REG_DWORD /d 1 /f
reg add HKLM\SOFTWARE\Policies\Microsoft\office\16.0\common\officeupdate /v hideenabledisableupdates /t REG_DWORD /d 1 /f

* FSLogix profile container

reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v MaxMonitors /t REG_DWORD /d 4 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v MaxXResolution /t REG_DWORD /d 5120 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v MaxYResolution /t REG_DWORD /d 2880 /f

reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\rdp-sxs" /v MaxMonitors /t REG_DWORD /d 4 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\rdp-sxs" /v MaxXResolution /t REG_DWORD /d 5120 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\rdp-sxs" /v MaxYResolution /t REG_DWORD /d 2880 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" /v RemoteAppLogoffTimeLimit /t REG_DWORD /d 0 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" /v fResetBroken /t REG_DWORD /d 1 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" /v MaxConnectionTime /t REG_DWORD /d 10800000 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" /v RemoteAppLogoffTimeLimit /t REG_DWORD /d 0 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" /v MaxDisconnectionTime /t REG_DWORD /d 5000 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" /v MaxIdleTime /t REG_DWORD /d 10800000 /f

Install Business Apps

In this case we will just install Visual Studio Code with the powershell extension:

Cleanup

Now clean up (also empty the recycle bin) all downloaded files, and optionally reboot the vm if you like to.

Create a SnapShot

In case you might want to go back to this point to install additional software you can create a https://docs.microsoft.com/en-us/azure/virtual-machines/windows/snapshot-copy-managed-disk

Not that you can view a list of snaphost in the Azure Potal by searching and selecting for snapshot. It also shows in the

SysPrep the Golden Image VM

Create an Image

Now we can create an image from the Golden Image VM. Note that this can not be undone and makes the VM unusable

Deploy a Windows Virtual Desktop HostPool with the Master Image

We can now create a hostpool based on the VM. This is almost identical to the hostpool we create before. See below for the steps, and note that the differences are in bold.

Create a hostpool to host the VMs that will be acting as the Windows Virtual Desktop.

Connect to the Custom Desktop

Startup a private chrome broweser window, connect to https://rdweb.wvd.microsoft.com/webclient and login using the sjoerdtest@aadtest001getshifting.onmicrosoft.com account.

After logging in you first see the Virtual Desktop Tenant: Virtualdesktoptest001, which now holds the Published Remote Apps and the Desktop VSCode-Desktop.

The powershell extension is not available in Visual Studio Code, so this might a per user installation.

Use SnapShot to Revert VM to VM

Unfortunately, you can't use the azure portal to restore a VM from the snapshot. And you also can't just reuse the VM. You can use powershell to create a new VM using the snapshot. You can use the powershell script below in Cloud Shell, as we have used before.

Connect-AzureAD
#Provide the subscription Id
$subscriptionId = 'c18b1986-f82f-4809-838a-39cfe062f21b'
#Provide the name of your resource group
$resourceGroupName ='RG_AzureADDS'
#Provide the name of the snapshot that will be used to create OS disk
$snapshotName = '20200312-PreSysPrep'
#Provide the name of the OS disk that will be created using the snapshot
$osDiskName = 'vm-we-w10-img02_OsDisk_1'
#Provide the name of an existing virtual network where virtual machine will be created
$virtualNetworkName = 'aadds-vnet'
#Provide the name of the virtual machine
$virtualMachineName = 'vm-we-w10-img02'
#Provide the size of the virtual machine
# get-azvmsize -location westeurope
$virtualMachineSize = 'Standard_B2s'
#Set the context to the subscription Id where Managed Disk will be created
Select-AzSubscription -SubscriptionId $SubscriptionId
$snapshot = Get-AzSnapshot -ResourceGroupName $resourceGroupName -SnapshotName $snapshotName
$diskConfig = New-AzDiskConfig -Location $snapshot.Location -SourceResourceId $snapshot.Id -CreateOption Copy
$disk = New-AzDisk -Disk $diskConfig -ResourceGroupName $resourceGroupName -DiskName $osDiskName
#Initialize virtual machine configuration
$VirtualMachine = New-AzVMConfig -VMName $virtualMachineName -VMSize $virtualMachineSize
#Use the Managed Disk Resource Id to attach it to the virtual machine.
$VirtualMachine = Set-AzVMOSDisk -VM $VirtualMachine -ManagedDiskId $disk.Id -CreateOption Attach -Windows
#Get the virtual network where virtual machine will be hosted
$vnet = Get-AzVirtualNetwork -Name $virtualNetworkName -ResourceGroupName $resourceGroupName
# Create NIC in the first subnet of the virtual network
$nic = New-AzNetworkInterface -Name ($VirtualMachineName.ToLower()+'_nic') -ResourceGroupName $resourceGroupName -Location $snapshot.Location -SubnetId $vnet.Subnets[0].Id
# Add the nic to the virtual machine
$VirtualMachine = Add-AzVMNetworkInterface -VM $VirtualMachine -Id $nic.Id
#Create the virtual machine with Managed Disk
New-AzVM -VM $VirtualMachine -ResourceGroupName $resourceGroupName -Location $snapshot.Location
Note that this connects the VM to the first subnet: $vnet.Subnets[0].Id . You could check with $vnet what the id is of the VDI subnet and use that, or change the subnet afterwards.

Then when the machine is done creating, you can test the VM by starting it and logging in (through Bastion). Note that the original local credentials are in use that were originally were used to install the VM vm-we-w10-img01. This means that the powershell extension in VSCode is also back!

Clean up Resources

Total Costs

The total costs of creating this over the course of about 10 days was 37,84 euro:

Follow these guidelines to minimize the costs, especially for test environments.

Lessons Learned

Reources