Intro
In this short post, I will make the change I promised in the last part of the series, allowing the use of images from the Compute Gallery. The change is minor, but it is essential when working with AVD, since most AVD environments use custom images with multisession host pools.
Prerequisites
You will need to be up to date with the environment created in the previous parts of this series. The code is still part of the repository; build it now if you haven’t already.
Changes to avd_session_host.bicep file
Let’s have a look at the changes I have made to the session host Bicep files.
I have added the option to specify the custom image reference in the parameters section. The custom image is optional, so the parameters are as well.
I have added these parameters to the parameters section of the Bicep file.
param custom_image_name string = ''
param custom_image_resource_group_name string = ''
param custom_image_version string = ''
param custom_image_galery_name string = ''
Next, I have added a lookup to find the image and version if specified in the parameters.
resource customImageGallery 'Microsoft.Compute/galleries@2024-03-03' existing = if (custom_image_galery_name != '') {
name: custom_image_galery_name
scope: resourceGroup(custom_image_resource_group_name)
}
resource customImage 'Microsoft.Compute/galleries/images@2024-03-03' existing = if (custom_image_galery_name != '' && custom_image_name != '') {
name: custom_image_name
parent: customImageGallery
}
resource customSigImageVersion 'Microsoft.Compute/galleries/images/versions@2024-03-03' existing = if (custom_image_galery_name != '' && custom_image_name != '' && custom_image_version != '') {
name: custom_image_version
parent: customImage
}
Now I want to use the custom image when I specify the parameters.
storageProfile: {
imageReference: (customSigImageVersion.id != null) ? {
id: customSigImageVersion.id
} : {
publisher: 'MicrosoftWindowsDesktop'
offer: 'office-365'
sku: 'win11-25h2-avd-m365'
version: 'latest'
}
osDisk: {
createOption: 'FromImage'
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
}
I can now update the session_hosts.bicep file I have in the Level5 root folder, and pass the parameters to the modules. The Bicep file now looks like this.
targetScope = 'subscription'
param availabilityset_name string = 'avail-level5'
param domain string = 'cloudninja.nu'
param domain_join_username string = 'svc_domainjoin@cloudninja.nu'
param domain_type string = 'EntraID'
@allowed([
'Production'
'Test'
])
param environment string = 'Production'
param host_pool_name string = 'level5'
param local_admin_username string = 'localadmin'
param key_vault_name string = 'kv-level5'
param key_vault_resource_group_name string = 'rg-level5-shared-services'
param key_vault_subscription_id string = 'f3b45d0c-2db9-498e-b885-9176d11d690c'
param location string = 'WestEurope'
param name string = 'level5'
param ou_path string = ''
param session_hosts_count int = 1
param subnet_name string = 'snet-avd-cloudninja-p'
param tags object = {
Owner: 'Martin'
Environment: environment
}
param virtual_network_name string = 'vnet-avd-p'
param virtual_network_resource_group_name string = 'rg-avd-network-p'
param vm_prefix string = 'level5'
param vm_size string = 'Standard_D2s_v3'
resource rg_hostpool 'Microsoft.Resources/resourceGroups@2024-07-01' existing = {
name: 'rg-${name}'
}
resource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: key_vault_name
scope: resourceGroup(key_vault_subscription_id,key_vault_resource_group_name)
}
module avd_session_hosts 'Modules/avd_session_host.bicep' = {
name: 'avd_session_hosts'
scope: rg_hostpool
params: {
tags: tags
subnet_name: subnet_name
availabilityset_name: availabilityset_name
custom_image_galery_name: 'gal_avd'
custom_image_name: 'w11_multiuser_packer'
custom_image_resource_group_name: 'rg-avd-sharedservices-p'
domain: domain
domain_join_username: (domain_type == 'EntraID') ? '' : domain_join_username
domain_join_password: (domain_type == 'EntraID') ? '' : kv.getSecret('domain-join-password')
host_pool_name: host_pool_name
local_admin_password: kv.getSecret('local-admin-password')
local_admin_username: local_admin_username
location: location
ou_path: ou_path
session_hosts_count: session_hosts_count
virtual_network_name: virtual_network_name
virtual_network_resource_group_name: virtual_network_resource_group_name
vm_prefix: vm_prefix
vm_size: vm_size
domain_type: domain_type
}
}
Deployment
I led the intro with the prerequisites, with what needed to be in place before running the new session host code, but let’s see how to deploy the whole solution.
New-AzSubscriptionDeployment -Name "AVD-Infrastructure" -Location "WestEurope" -TemplateFile .\main.bicep -Verbose
New-AzSubscriptionDeployment -Name "AVD-SessionHosts" -Location "WestEurope" -TemplateFile .\session_hosts.bicep -Verbose
Login to AVD
With the AVD infrastructure and session host running, I can log in to the AVD environment with my test user and start using the applications I have in the customer image.
GitHub repository
I have uploaded all the code for this blog series to my public GitHub repository. You can find it here:
Summary
In this post, I have added a vital part of AVD environments: the custom image option. The code change is small, but the impact is significant. I have demonstrated how to deploy the entire environment, and we now have a running AVD session host using the custom image defined. I also added the option to add an Entra Group to the application group so that I don’t have to do this manually.
Since the AVD environment is running with a custom image, it is tempting to say we are done, but we haven’t configured stuff like FSLogix profiles, custom RDP options, or even host pool properties. These settings will be for a future blog post.
As always, feedback is welcome, and if I missed some points, please let me know.