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:

AVD on GitHub

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.