Intro

In the first two blog posts (here) and (here), I wrote the most basic Bicep code for deploying a host pool, application groups, and a workspace. In this post, I want to go through some of the resources I usually designate as shared resources, meaning the workspace and the storage account. I already had the workspace in the last two articles, but this time, I am moving those resources into a shared resource group since multiple host pools often use the workspace.

Main Bicep file

I am creating two resource groups in the code below: one for a host pool and application groups, which will later contain session hosts, and one for shared services. After creating the two resource groups, I call a module to deploy the host pool and application groups. After that, I call another module to create the shared services, which are the workspace and storage account.

targetScope = 'subscription'

@allowed([
  'Production'
  'Test'
])
param environment string = 'Production'
param location string = 'WestEurope'
param name string = 'level3'
param tags object = {
  Owner: 'Martin'
  Environment: environment
}

resource rg_hostpool 'Microsoft.Resources/resourceGroups@2024-07-01' = {
  name: 'rg-${name}'
  location: location
  tags: tags
}

resource rg_shared_services 'Microsoft.Resources/resourceGroups@2024-07-01' = {
  name: 'rg-${name}-shared-services'
  location: location
  tags: tags
}

module avd_hostpool 'Modules/avd_hostpool.bicep' = {
  name: 'avd_hostpool'
  scope: rg_hostpool
  params: {
    location: location
    name: name    
    tags: tags
  }
}

module avd_shared_services 'Modules/avd_shared_services.bicep' = {
  name: 'avd_shared_services'
  scope: rg_shared_services
  params: {
    location: location
    name: name    
    tags: tags
    create_storage_account: true
    desktop_dag: avd_hostpool.outputs.desktop_dag
    remote_app_dag: avd_hostpool.outputs.remote_app_dag
  }
}

AVD Host pool module

I have split the AVD host pool and application groups into a separate module. This split is done to have shared services in its own module so that resources are naturally colocated. The code below creates the host pool and application groups, and it should be familiar since I used almost the same code in the first two parts of this blog series.

@allowed([
  'Pooled'
  'Personal'
])
param hostPoolType string = 'Pooled'

@allowed([
  'BreadthFirst'
  'DepthFirst'
])
param loadBalancerType string = 'DepthFirst'
param location string
param name string
@allowed([
  'Desktop'
  'RemoteApp'
])
param preferredAppGroupType string = 'Desktop'
param tags object

resource hostpool 'Microsoft.DesktopVirtualization/hostPools@2024-08-08-preview' = {
  name: 'vdpool-${name}'
  location: location
  properties: {
    hostPoolType: hostPoolType
    loadBalancerType: loadBalancerType
    preferredAppGroupType: preferredAppGroupType
  }
  tags: tags
}

resource desktop_dag 'Microsoft.DesktopVirtualization/applicationGroups@2024-08-08-preview' = {
  name: 'vdag-${name}-desktop'
  location: location
  properties: {
    applicationGroupType: 'Desktop'
    friendlyName: '${name} Desktop'    
    hostPoolArmPath: resourceId('Microsoft.DesktopVirtualization/hostpools', hostpool.name)
  }
  tags: tags
}

resource remote_app_dag 'Microsoft.DesktopVirtualization/applicationGroups@2024-08-08-preview' = {
  name: 'vdag-${name}-remoteapp'
  location: location
  properties: {
    applicationGroupType: 'RemoteApp'
    friendlyName: '${name} remote app'    
    hostPoolArmPath: resourceId('Microsoft.DesktopVirtualization/hostpools', hostpool.name)
  }
  tags: tags
}

output desktop_dag string = resourceId('Microsoft.DesktopVirtualization/applicationGroups', desktop_dag.name)
output remote_app_dag string = resourceId('Microsoft.DesktopVirtualization/applicationGroups',remote_app_dag.name)

Shared services module

The shares services module creates the AVD workspace and the storage account. I have an option in the module to deploy the storage account. This option allows multiple host pools to use the same profile storage account. I have set the option to “false” by default, but this setting varies much between customers. There can be reasons for always creating a storage account to separate profiles or knowing in advance that each host pool will have too many users to allow for sharing the storage account. As with the previous post, I use several option parameters to enable an easy default deployment.

// Shared parameters
param location string
param name string
param tags object

// Storage account params
// Shared parameters
param location string
param name string
param tags object

// Storage account params
param create_storage_account bool = false
param desktop_dag string
param fileshare_name string = 'profiles'
@allowed([
  'BlobStorage'
  'BlockBlobStorage'
  'FileStorage'
  'Storage'
  'StorageV2'
])
param kind string = 'FileStorage'
param remote_app_dag string
@allowed([
  'Standard_LRS'
  'Standard_GRS'
  'Standard_RAGRS'
  'Standard_ZRS'
  'Premium_LRS'
])
param sku string = 'Premium_LRS'

resource workspace 'Microsoft.DesktopVirtualization/workspaces@2024-08-08-preview' = {
  name: 'vdws-${name}'
  location: location
  properties: {
    applicationGroupReferences: [
      desktop_dag
      remote_app_dag
    ]
    description: '${name} Workspace' 
    friendlyName: '${name} workspace'    
  }
  tags: tags
}

module storage_account 'avd_storage_account.bicep' = if (create_storage_account)  {
  name: 'storage_account'
  params: {
    fileshare_name: fileshare_name
    kind: kind
    location: location
    name: name
    tags: tags    
    sku: sku
  }
}

Storage account module

The storage account has its module; this is not nessary, but splitting it up makes sense to keep the shared services module cleaner. It also enables me to reuse the storage account module in other services than the AVD one. The storage account module will create a storage account and a file share. To make the file share, I first need to use the file services resources, allowing me to create the file share.

Only name and location are required parameters, but note that if you use the code below, you will provision a premium storage account, which can be expensive.

param fileshare_name string = 'profiles'
@allowed([
  'BlobStorage'
  'BlockBlobStorage'
  'FileStorage'
  'Storage'
  'StorageV2'
])
param kind string = 'FileStorage'
param location string
param name string
param tags object
@allowed([
  'Standard_LRS'
  'Standard_GRS'
  'Standard_RAGRS'
  'Standard_ZRS'
  'Premium_LRS'
])
param sku string = 'Premium_LRS'

resource stg 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: 'sa${name}${uniqueString(resourceGroup().id)}'
  location: location
  kind: kind
  properties: {
    allowBlobPublicAccess: false  
    minimumTlsVersion: 'TLS1_2'
    supportsHttpsTrafficOnly: true      
  }  
  sku: {
    name: sku
  }
  tags: tags
}
resource file 'Microsoft.Storage/storageAccounts/fileServices@2023-05-01' = {
  name: 'default'
  parent: stg
  properties: {
    protocolSettings: {
      smb: {}
    }
  }
}

resource fileshare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-05-01' = {
  name: fileshare_name
  parent: file
  properties: {
    shareQuota: 100
  }
}

Storage account configuration

When using FSLogix, the storage account needs to be configured to

These steps are from the Microsoft guide for the configuration of the storage account for Active Directory.

  1. Enable Active Directory authentication

  2. Enable share-level permissions

  3. Assign directory/file-level permissions

  4. Mount files share using AD credentials

I feel the Microsoft documentation is clear, but if you would like me to elaborate on this post with the complete guide to configuring the storage account, let me know.

Summary

In this blog post, I have optimized the code from the previous posts, allowing me to split services into host pool and shared services. I like this split since smaller customers can share workspace and storage accounts, and having multiple host pools in the same resource group doesn’t look right. I prefer having host pools, application groups, and session hosts in dedicated resource groups since managing the resources feels simpler. I continue to have many optional parameters, which enables me to deploy entire environments with very few inputs; this should ensure that environments are configured similarly and, therefore, have less troubleshooting due to configuration.

The basic resources for AVD are now deployed, but we can still add configurations and additions to the code. If you like this series so far, please let me know, and also let me know if you are missing something from the posts so far.