Deploy Azure Virtual Desktop (AVD) on Azure Stack HCI

This post will be a short guide through all steps without HCI Setup.


After you have deployed your HCI Cluster successfully you will have your cluster resources within the azure portal. Once you click on that cluster you will find the following overview.

As you can see as well all prerequisites are met. These prerequisites are:


Now you can click on “DEPLOY” to start a custom deployment:

most of the informations are clear, but these 3 were a bit tricky for me 😉


The location you will find within your Azure ARC resources. (Azure Portal > Azure Arc > Custom Location > Properties > ID


To finde the Image id it is required to add at least one image to azure stack.

You have three options to add an image:

The easiest way to get started is to add an azure marketplace image. I have already added “Windows 11” and “Windows Server” to my list. After adding an image go to azure portal > azure stack hci > vm image > “windows11” now copy the url from your browser – that must look like this:

remove /overview at the end und copy that url to your custom deployment.


Go to Azure Stack HCI > Your HCI Stack > virtual networks > and copy the browser URL that must look like this:

and add your virtual network name to the end like this:

Issues during deployment

My first deployments failed and I wasn’t sure why. After I checked the deployments within my resource group and checked my inputs to the last failed one

I found that my VM tries to get access to the following URL. That was blocked so I copied that script and created my own https url as a workarround.

To change that URL only “redeploy” of one of the last deployments gives you the option to change that URL

After Deployment

After that deployment I had my VM up and running on my azure stack hci. It was domain joined but the avd agent was mising. I installed that avd agent manually. Now I was able to see that host within the azure portal.

Successful Connection

and here we go I was able to get a connection

FSLogix Profile Container with Azure Files and AzureAD

I have already written a post about how to deploy AVD with AzureAD joined VMs

Addition to AzureAD joined VMs we need an Azure Storage Account, an Azure File share as well to save our FSLogix Profiles. This post will guide you to all steps.

At time of writing AzureFiles and AzureAD Kerberos functionality is still in preview (


  • User must be a hybrid user identities!!!!!!
  1. you must have an Azure Subscription and ADConnect already installed and configured
  2. create a storage account
    I used the following settings and leaved all other settings by default…

3. create azure file share

for my lab I only use SKU standard but for prod environments I always use and recommend premium storage. Microsoft created insights on sizing and designing fslogix solutions for enterprises here:

4. enable Azure AD authorization

Connect-AzAccount -Tenant $tenantId -SubscriptionId $subscriptionId
$Uri = ('{0}/resourceGroups/{1}/providers/Microsoft.Storage/storageAccounts/{2}?api-version=2021-04-01' -f $subscriptionId, $resourceGroupName, $storageAccountName);
$json = @{properties=@{azureFilesIdentityBasedAuthentication=@{directoryServiceOptions="AADKERB"}}};
$json = $json | ConvertTo-Json -Depth 99
$token = $(Get-AzAccessToken).Token
$headers = @{ Authorization="Bearer $token" }
try {
Invoke-RestMethod -Uri $Uri -ContentType 'application/json' -Method PATCH -Headers $Headers -Body $json;
} catch {
Write-Host $_.Exception.ToString()
Write-Error -Message "Caught exception setting Storage Account directoryServiceOptions=AADKERB: $_" -ErrorAction Stop
New-AzStorageAccountKey -ResourceGroupName $resourceGroupName -Name $storageAccountName -KeyName kerb1 -ErrorAction Stop

5. create an Azure AD application that represent the storage account (during preview only via Powershell)

Install-Module -Name Az.StorageInstall-Module -Name AzureAD
$tenantid = "yourid"
$subscriptionid = "yourid"
$resourceGroupName = "rg"
$storageAccountName = "name"
#set password between storage account and azuread
$kerbKey1 = Get-AzStorageAccountKey -ResourceGroupName $resourceGroupName -Name $storageAccountName -ListKerbKey | Where-Object { $_.KeyName -like "kerb1" }
$aadPasswordBuffer = [System.Linq.Enumerable]::Take([System.Convert]::FromBase64String($kerbKey1.Value), 32);
$password = "kk:" + [System.Convert]::ToBase64String($aadPasswordBuffer);
#connect to tennant

Set-AzContext -Subscription $subscriptionid
$azureAdTenantDetail = Get-AzureADTenantDetail;
$azureAdTenantId = $azureAdTenantDetail.ObjectId
$azureAdPrimaryDomain = ($azureAdTenantDetail.VerifiedDomains | Where-Object {$_._Default -eq $true}).Name

#generate service principal names

$servicePrincipalNames = New-Object string[] 3
$servicePrincipalNames[0] = 'HTTP/{0}' -f $storageAccountName
$servicePrincipalNames[1] = 'CIFS/{0}' -f $storageAccountName
$servicePrincipalNames[2] = 'HOST/{0}' -f $storageAccountName

#create app

$application = New-AzureADApplication -DisplayName $storageAccountName -IdentifierUris $servicePrincipalNames -GroupMembershipClaims "All";

#create service

$servicePrincipal = New-AzureADServicePrincipal -AccountEnabled $true -AppId $application.AppId -ServicePrincipalType "Application";

$Token = ([Microsoft.Open.Azure.AD.CommonLibrary.AzureSession]::AccessTokens['AccessToken']).AccessToken
$Uri = ('{0}/{1}/{2}?api-version=1.6' -f $azureAdPrimaryDomain, 'servicePrincipals', $servicePrincipal.ObjectId)
$json = @'
"passwordCredentials": [
"customKeyIdentifier": null,
"endDate": "",
"value": "",
"startDate": ""
$now = [DateTime]::UtcNow
$json = $json -replace "", $now.AddDays(-1).ToString("s")
$json = $json -replace "", $now.AddMonths(6).ToString("s")
$json = $json -replace "", $password
$Headers = @{'authorization' = "Bearer $($Token)"}
try {
Invoke-RestMethod -Uri $Uri -ContentType 'application/json' -Method Patch -Headers $Headers -Body $json
Write-Host "Success: Password is set for $storageAccountName"
} catch {
Write-Host $_.Exception.ToString()
Write-Host "StatusCode: " $_.Exception.Response.StatusCode.value
Write-Host "StatusDescription: " $_.Exception.Response.StatusDescription

Result is an app with the name of our storage account:

and you can see on your storage account a hint that kerberos is enabled.

6. API Permissions

now you can assign API permissions to that app:

1st we need openid permissions

  • openid – Sign user in
  • profile – View users basic profile

2nd we need User permissions

  • User.Read – Sign in and read user profile

after adding it is important to click “Grant admin consent”. For this you need right permissions within AzureAD.

7. Assign SMB Permissions

To allow an User to create profile container we need to assign RBAC Roles to the Storage account.

Storage File Data SMB Share Contributor for all AVD Users and “Elevated Contributor” to a Group or Users that need Admin Permission.

8. Assign directory level access permissions

For me it was quiet confusing where I have to set these permisions and which system I have to use. In my Lab I have one Windows VM with AD DS and ADConnect installed that I used to run that scripts.

From MS Documentation I found:

The system you use to configure the permissions must meet the following requirements:

  • The version of Windows meets the supported OS requirements defined in the Prerequisites section.
  • Is Azure AD-joined or Hybrid Azure AD-joined to the same Azure AD tenant as the storage account.
  • Has line-of-sight to the domain controller.
  • Is domain-joined to your Active Directory (Windows Explorer method only).
Connect-AzAccount -Tenant $tenantId -SubscriptionId $subscriptionId
$AdModule = Get-Module ActiveDirectory;
if ($null -eq $AdModule) {
Write-Error "Please install and/or import the ActiveDirectory PowerShell module." -ErrorAction Stop;
$domainInformation = Get-ADDomain
$Domain = $domainInformation.DnsRoot
$domainGuid = $domainInformation.ObjectGUID.ToString()
$domainName = $domainInformation.DnsRoot
$domainSid = $domainInformation.DomainSID.Value
$forestName = $domainInformation.Forest
$netBiosDomainName = $domainInformation.DnsRoot
$azureStorageSid = $domainSid + "-123454321";
Write-Verbose "Setting AD properties on $storageAccountName in $resourceGroupName : EnableActiveDirectoryDomainServicesForFile=$true, ActiveDirectoryDomainName=$domainName,
ActiveDirectoryNetBiosDomainName=$netBiosDomainName, ActiveDirectoryForestName=$($domainInformation.Forest) ActiveDirectoryDomainGuid=$domainGuid, ActiveDirectoryDomainSid=$domainSid,
$Uri = ('{0}/resourceGroups/{1}/providers/Microsoft.Storage/storageAccounts/{2}?api-version=2021-04-01' -f $subscriptionId, $resourceGroupName, $storageAccountName);
$json = $json | ConvertTo-Json -Depth 99
$token = $(Get-AzAccessToken).Token
$headers = @{ Authorization="Bearer $token" }
try {
Invoke-RestMethod -Uri $Uri -ContentType 'application/json' -Method PATCH -Headers $Headers -Body $json
} catch {
Write-Host $_.Exception.ToString()
Write-Host "Error setting Storage Account AD properties. StatusCode:" $_.Exception.Response.StatusCode.value__
Write-Host "Error setting Storage Account AD properties. StatusDescription:" $_.Exception.Response.StatusDescription
Write-Error -Message "Caught exception setting Storage Account AD properties: $_" -ErrorAction Stop

9. Enable AzureAD functionality via Registry

reg add HKLM\SYSTEM\CurrentControlSet\Control\Lsa\Kerberos\Parameters /v CloudKerberosTicketRetrievalEnabled /t REG_DWORD /d 1

10. Configure SessionHost

On my Sessionhost I configured FSLogix like this:

Because I used that host already with my test account I already had a local profile. Within my first test it was not working, but with allowing FSLogix to delete lcoal profile than I was able to login and my profile container was created.

To be honest I had some trouble to get this working. Maybe it was one of my reboot to solve this 😉

Thanks for reading

AVD “Start on Connect”

When a Virtual Machine is running we have to pay for using CPU and RAM. When we are able to turn off (deallocate) a Virtual Machine then we can save costs.

With “Start on Connect” feature we can allow end users to turn on AVD Hosts and if they log off we can deallocate our hosts again. The result can be that we save costs!

I will go through all task to enable that feature.

1. Create and Assign Custom Role

First of all we need to have a custom role that will be used to start our hosts.

Subscription > Access control > add > custom role

Now we can create a role assignment. I did this on my Ressource Group “rg-avd” where my host is located.

our next step is to look for our new created custom role. if you can not find this, please try to refresh your session.

next part is to define members – here we need to find “Windows Virtual Desktop”

If you can not see any member then your user must be assigned to the security administrator role.

2. Enable “Start on Connect” on Hostpool

Let’s try if its working. My pool only one host and this host is deallocated.

Lets start our SessionDesktop:

Our Client will wait until one host is up and connected to the avd service.

Within the activity logs we can check who initiated the start of my host.

It was initiated by “Azure Virtual Desktop” as designed .

Thats it and thx for reading

Azure Virtual Desktop and AzureAD joined VM

Since some time it is possible to join a Windows VM to Azure AD directly. Now this is also possible with Azure Virtual Desktop.

This Blogpost will show all my steps until I am possible to login to my Windows 10 System.


Create a host pool

First of all we need some basic informations such as pool name.

next to the basics we need to define: VM Size, VM Availability, Image type and the number of VMs.

General Settings

addition to that we can use an existing network or we are able to create a new one.

Network Settings

After these Settings we need to define which domain we want to join. Her we can now choose between Active Directory and Azure Active Directory.

I have chosen AzureAD.


During the host pool creation it is possible to create a assignment to a workspace. I have created tech-guy-workspace as a new one.

Roles and Permissions

With AzureAD joined devices we need to create a role assignment and a app group assignment. With each host pool one default app group will be created. In my test lab it is called “tech-guys-personal-pool-DAG”.

default app group

within this app group we are able to assign users

2nd task is to assign rbac role to at least the virtual machine to that we want to login. I prefer to assign that role to my resource group that I have that assignment for all future host as well.

there are 2 roles we need to consider about.

RBAC Roles

As it says the first role is useful when you want to login and want to have admin privileges on that machine. second group is only for your users that they are able to login without admin permission. In my lab I assigned my test user to “virtual machine user login” and my cloud only user “virtual machine administrator login” role.

To access host pool VMs, your local computer must be:

  • Azure AD-joined or hybrid Azure AD-joined to the same Azure AD tenant as the session host.
  • Running Windows 10 version 2004 or later, and also Azure AD-registered to the same Azure AD tenant as the session host.

Host pool access uses the Public Key User to User (PKU2U) protocol for authentication. To sign in to the VM, the session host and the local computer must have the PKU2U protocol enabled. For Windows 10 version 2004 or later machines, if the PKU2U protocol is disabled, enable it in the Windows registry as follows:

  1. Navigate to HKLM\SYSTEM\CurrentControlSet\Control\Lsa\pku2u.
  2. Set AllowOnlineID to 1

and here we go.

If you need to use an other client rather than the windows one, than you need enable the RDSTLS protocol. Just add a new custom RDP Property to the host pool, targetisaadjoined:i:1. Azure Virtual Desktop then uses this protocol instead of PKU2U.