Trying Windows Virtual Desktop

Update: Windows Virtual Desktop 2020 Spring Update brings an new fully ARM-based deployment method and portal experience. As of May 2020, these features are in public preview. Check the documentation. Links to the documentation in this article will also reflect the update.

It was Sunday afternoon. I had some time. So I had this crazy idea to try out Windows Virtual Desktop. Just so you know, I am not really into “the desktop” and all its intricacies. I have done my fair share of Remote Desktop Services, App-V and Citrix in the past but that is probably already a decade ago.

Before moving on, review the terminology like tenants, host pools, app groups, etc…. That can be found here: https://docs.microsoft.com/en-us/azure/virtual-desktop/environment-setup

To start, I will share the (wrong) assumptions I made:

  • “I will join the virtual desktops to Azure Active Directory directly! That way I do not have to set up domain controllers and stuff. That must be supported!” – NOPE, that is not supported. You will need domain controllers synced to the Azure AD instance you will be using. You can use Azure AD Domain Services as well.
  • “I will just use the portal to provision a host pool. I have seen there’s a marketplace item for that called Windows Virtual Desktop – Provision a host pool” – NOPE, it’s not meant to work just like that. Move to the next point…
  • “I will not read the documentation. Why would I? It’s desktops we’re talking about, not Kubernetes or Istio or something!” πŸ˜‰

Let’s start with that last assumption shall we? You should definitely read the documentation, especially the following two pages:

The WVD Tenant

Use the link above to create the tenant and follow the instructions to the letter. A tenant is a group of one or more host pools. The host pools contain desktops and servers that your users will connect to. The host pool provisioning wizard in the portal will ask for this tenant.

You will need Global Admin rights in your Azure AD tenant to create this Windows Virtual Desktop tenant. That’s another problem I had since I do not have those access rights at my current employer. I used an Azure subscription tied to my employer’s Azure AD tenant. To fix that, I created an Azure trial with a new Azure AD tenant where I had full control.

Another problem I bumped into is that the account I created the Azure AD tenant with is an account in the baeke.info domain which will become an external account in the directory. You should not use such an account in the host pool provisioning wizard in the portal because it will fail.

When the tenant is created, you can use PowerShell to get information about it (Ids were changed to protect the innocent):

TenantGroupName : Default Tenant Group
AadTenantId : 1a887615-efcb-2022-9279-b9ada644332c
TenantName : BaekeTenant
Description :
FriendlyName :
SsoAdfsAuthority :
SsoClientId :
SsoClientSecret :
SsoClientSecretType : SharedKey
AzureSubscriptionId : 4d29djus-d120-4bac-8681-5e5e33ab77356
LogAnalyticsWorkspaceId :
LogAnalyticsPrimaryKey :

Great, my tenant is called BaekeTenant and the tenant group is Default Tenant Group. During host pool provisioning, Default Tenant Group is automatically proposed as the tenant group.

Service Principals and role assignments

The service principal is used to automate certain Windows Virtual Desktop management tasks. Follow https://docs.microsoft.com/en-us/azure/virtual-desktop/create-service-principal-role-powershell to the letter to create this service principal. It will need a specific role called RDS Owner, via the following PowerShell command (where $svcPrincipal and $myTenantName are variables from previous steps):

New-RdsRoleAssignment -RoleDefinitionName "RDS Owner" -ApplicationId $svcPrincipal.AppId -TenantName $myTenantName

If that role is not granted, all sorts of things might happen. I had an error after domain join, in the dscextension resource. You can check the ARM deployment for that. It’s green below but it was red quite a few times because I used an account that did not have the role:

So, if that step fails for you, you know where to look. If the joindomain resource fails, it probably has to do with the WVD hosts not being able to find the domain controllers. Check the DNS settings! Also check the AD Join section below.

During host pool provisioning, you will need the following in the last step of the wizard (see further down below):

  • The application ID: the user name of the service principal ($svcPrincipal.AppId)
  • The secret: the password of the service principal
  • The Azure AD tenant ID: you can find it in the Properties page of your Azure AD tenant

Active Directory Join

When you provision the host pool, one of the provisioning steps is joining the Windows 10 or Windows Server system to Active Directory. An Azure Active Directory Join is not supported. Because I did not want to install and configure domain controllers, I used Azure AD Domain Services to create a domain that syncs with my new Azure AD tenant:

Using Azure AD Domain Services: Windows 10 systems in the host pool will join this AD

Of course, you need to make sure that Windows Virtual Desktop machines, use DNS servers that can resolve requests for this domain. You can find this in Properties:

DNS servers to use are 10.0.0.4 and 10.0.0.5 in my case

The virtual network that contains the subnet with the virtual desktops, has the following setting for DNS:

DNS servers of VNET so virtual desktop hosts in the VNET can be joined to the Azure AD Domain Services domain

During host pool provisioning, you will be asked for an account that can do domain joins. You can also specify the domain to join if the domain suffix of the account you specify is different. The account you use should be member of the following group as well:

Member of AAD DC Administrators group

When you have all of this stuff in place, you are finally ready to provision the host pool. To recap:

  • Create a WVD tenant
  • Create a Service Principal that has the RDS Owner role
  • Make sure WVD hosts can join Active Directory (Azure AD join not supported); you can use Azure AD Domain Services if you want
  • Make sure WVD hosts use DNS servers that can resolve the necessary DNS records to find the domain controllers; I used the IP addresses of the Azure AD Domain Services domain controllers here
  • Make sure the account you use to join the domain is member of the AAD DC Administrators group

When all this is done, you are finally ready to use the marketplace item in the portal to provision the host pool:

Use the search bar to find the WVD marketplace item

Here are the screens of how I filled them out:

The basics

You can specify more users. Use a comma as separator. I only added my own account here. You can add more users later with the Add-RdsAppGroupUser, to add a user to the default Desktop Application Group. For example:

Add-RdsAppGroupUser -TenantName "BaekeTenant" -HostPoolName "baekepool" -AppGroupName "Desktop Application Group" -UserPrincipalName "other@azurebaeke.onmicrosoft.com"

And now the second step:

I only want 1 machine to test the features

Next, virtual machine settings:

VM settings

I use the Windows 10 Enterprise multi-session image from the gallery. Note you can use your own images as well. I don’t need to specify a domain because the suffix of the AD domain join UPN will be used: azurebaeke.onmicrosoft.com. The virtual network has DNS configured to use the Azure AD Domain Services servers. The domain join user is member of the AAD DC Administrators group.

And now the final screen:

At last….

The WVD BaekeTenant was created earlier with PowerShell. We also created a service principal with the RDS Owner role so we use that here. Part of this process is the deployment of the WVD session host(s) in the subnet you specified:

Deployed WVD session host

If you followed the Microsoft documentation and some of the tips in this post, you should be able to get to your desktop fairly easily. But how? Just check this to connect from your desktop, or this to connect from the browser. Just don’t try to use mstsc.exe, it’s not supported. End result: finally able to logon to the desktop…

Soon, a more integrated Azure Portal experience is coming that will (hopefully) provide better guidance with sufficient checks and tips to make this whole process a lot smoother.

Update to IoT Simulator

Quite a while ago, I wrote a small IoT Simulator in Go that creates or deletes multiple IoT devices in IoT Hub and sends telemetry at a preset interval. However, when you use version 0.4 of the simulator, you will encounter issues in the following cases:

  • You create a route to store telemetry in an Azure Storage account: the telemetry will be base 64 encoded
  • You create an Event Grid subscription that forwards the telemetry to an Azure Function or other target: the telemetry will be base 64 encoded

For example, in Azure Storage, when you store telemetry in JSON format, you will see something like this with versions 0.4 and older:

{"EnqueuedTimeUtc":"2020-02-10T14:13:19.0770000Z","Properties":{},"SystemProperties":{"connectionDeviceId":"dev35","connectionAuthMethod":"{\"scope\":\"hub\",\"type\":\"sas\",\"issuer\":\"iothub\",\"acceptingIpFilterRule\":null}","connectionDeviceGenerationId":"637169341138506565","contentType":"application/json","contentEncoding":"","enqueuedTime":"2020-02-10T14:13:19.0770000Z"},"Body":"eyJUZW1wZXJhdHVyZSI6MjYuNjQ1NjAwNTMyMTg0OTA0LCJIdW1pZGl0eSI6NDQuMzc3MTQxODcxODY5OH0="}

Note that the body is base 64 encoded. The encoding stems from the fact that UTF-8 encoding was not specified as can be seen in the JSON. contentEncoding is indeed empty and the contentType does not mention the character set.

To fix that, a small code change was required. Note that the code uses HTTP to send telemetry, not MQTT or AMQP:

Setting the character set as part of the content type

With the character set as UTF-8, the telemetry in the Storage Account will look like this:

{"EnqueuedTimeUtc":"2020-02-11T15:02:07.9520000Z","Properties":{},"SystemProperties":{"connectionDeviceId":"dev15","connectionAuthMethod":"{\"scope\":\"hub\",\"type\":\"sas\",\"issuer\":\"iothub\",\"acceptingIpFilterRule\":null}","connectionDeviceGenerationId":"637169341138088841","contentType":"application/json; charset=utf-8","contentEncoding":"","enqueuedTime":"2020-02-11T15:02:07.9520000Z"},"Body":{"Temperature":20.827852028684607,"Humidity":49.95058826575425}}

Note that contentEncoding is still empty here, but contentType includes the charset. That is enough for the body to be in plain text.

The change will also allow you to use queries on the body in IoT Hub message routing filters or Event Grid subscription filters.

Enjoy the new version 0.5! All three of you… πŸ˜‰πŸ˜‰πŸ˜‰