Azure Local – Custom Images – Compute Gallery and Image Builder

This is the first part of an article series. In this post we will be deploying a compute gallery and image builder config.

Compute Image Gallery

Use this Bicep template to deploy the compute gallery and image definition. You need to change the parameters to fit your setup before running the deployment:

@description('Parameters for Image Gallery Definition')
param computeGalleryName string = 'myComputeGallery'
param imageDefinitionName string = 'myImageDefinition'
param osType string = 'Windows'
param osState string = 'Generalized'
param publisher string = 'MyCompanyName'
param offer string = 'MyOfferName' // e.g.: Windows-11-Custom
param sku string = 'MySkuName' // e.g.: 24H2
param minRecommendedvCPUs int = 1
param maxRecommendedvCPUs int = 64
param minRecommendedMemory int = 8
param maxRecommendedMemory int = 256
param hyperVGeneration string = 'V2'
param IsAcceleratedNetworkSupported string = 'false' // Set to false cause of issues with capture
param architecture string = 'X64'
param location string = 'westeurope'

resource computegallery 'Microsoft.Compute/galleries@2022-03-03' = {
  name: computeGalleryName
  location: location
  properties: {
  }
}

resource galleryNameImageDefinition 'Microsoft.Compute/galleries/images@2021-10-01' = {
  parent: computegallery
  name: imageDefinitionName
  location: location
  properties: {
    osType: osType
    osState: osState
    identifier: {
      publisher: publisher
      offer: offer
      sku: sku
    }
    recommended: {
      vCPUs: {
        min: minRecommendedvCPUs
        max: maxRecommendedvCPUs
      }
      memory: {
        min: minRecommendedMemory
        max: maxRecommendedMemory
      }
    }
    hyperVGeneration: hyperVGeneration
    features: [
      {
        name: 'IsAcceleratedNetworkSupported'
        value: IsAcceleratedNetworkSupported
      }
    ]
    architecture: architecture
  }
}

Custom Image Builder

Deploying Custom Image Builder is a bit more complex. I have written a deployment PowerShell script to show you how you can deploy it.

First we need to prepare a customize file for all the settings for the image builder (hint: you can deploy custom image builder in the portal and then export the customize parameters)

Customize.bicep

var customize = [
  {
    destination: 'C:\\AVDImage\\installLanguagePacks.ps1'
    name: 'avdBuiltInScript_installLanguagePacks'
    sha256Checksum: '519f1dcb41c15dc1726f28c51c11fb60876304ab9eb9535e70015cdb704a61b2'
    sourceUri: 'https://raw.githubusercontent.com/Azure/RDS-Templates/master/CustomImageTemplateScripts/CustomImageTemplateScripts_2024-03-27/InstallLanguagePacks.ps1'
    type: 'File'
  }
  {
    inline: [
      'C:\\AVDImage\\installLanguagePacks.ps1 -LanguageList "Danish (Denmark)","English (United States)"'
    ]
    name: 'avdBuiltInScript_installLanguagePacks-parameter'
    runAsSystem: true
    runElevated: true
    type: 'PowerShell'
  }
  {
    name: 'avdBuiltInScript_installLanguagePacks-windowsUpdate'
    type: 'WindowsUpdate'
    updateLimit: 0
  }
  {
    name: 'avdBuiltInScript_installLanguagePacks-windowsRestart'
    restartTimeout: '10m'
    type: 'WindowsRestart'
  }
  {
    name: 'avdBuiltInScript_timeZoneRedirection'
    runAsSystem: true
    runElevated: true
    scriptUri: 'https://raw.githubusercontent.com/Azure/RDS-Templates/master/CustomImageTemplateScripts/CustomImageTemplateScripts_2024-03-27/TimezoneRedirection.ps1'
    sha256Checksum: 'b8dbc50b02f64cc7a99f6eeb7ada676673c9e431255e69f3e7a97a027becd8d5'
    type: 'PowerShell'
  }
  {
    destination: 'C:\\AVDImage\\enableFslogix.ps1'
    name: 'avdBuiltInScript_enableFsLogix'
    sha256Checksum: '027ecbc0bccd42c6e7f8fc35027c55691fba7645d141c9f89da760fea667ea51'
    sourceUri: 'https://raw.githubusercontent.com/Azure/RDS-Templates/master/CustomImageTemplateScripts/CustomImageTemplateScripts_2024-03-27/FSLogix.ps1'
    type: 'File'
  }
  {
    inline: [
      'C:\\AVDImage\\enableFslogix.ps1 -FSLogixInstaller "https://aka.ms/fslogix_download" -VHDSize "50000" -ProfilePath "\\\\share\\fslogix"'
    ]
    name: 'avdBuiltInScript_enableFsLogix-parameter'
    runAsSystem: true
    runElevated: true
    type: 'PowerShell'
  }
  {
    name: 'avdBuiltInScript_configureRdpShortpath'
    runAsSystem: true
    runElevated: true
    scriptUri: 'https://raw.githubusercontent.com/Azure/RDS-Templates/master/CustomImageTemplateScripts/CustomImageTemplateScripts_2024-03-27/RDPShortpath.ps1'
    sha256Checksum: '24e9821ddcc63aceba2682286d03cd7042bcadcf08a74fb0a30a1a1cd0cbf910'
    type: 'PowerShell'
  }
  {
    destination: 'C:\\AVDImage\\TeamsOptimization.ps1'
    name: 'avdBuiltInScript_teamsOptimization'
    sha256Checksum: 'b6e4b30185cb4eb556846ecf9951bacda29ef657230c6ad0924c7f49ab1f6975'
    sourceUri: 'https://raw.githubusercontent.com/Azure/RDS-Templates/master/CustomImageTemplateScripts/CustomImageTemplateScripts_2024-03-27/TeamsOptimization.ps1'
    type: 'File'
  }
  {
    inline: [
      'C:\\AVDImage\\TeamsOptimization.ps1 -WebRTCInstaller "https://aka.ms/msrdcwebrtcsvc/msi" -VCRedistributableLink "https://aka.ms/vs/17/release/vc_redist.x64.exe" -TeamsBootStrapperUrl "https://go.microsoft.com/fwlink/?linkid=2243204&clcid=0x409"'
    ]
    name: 'avdBuiltInScript_teamsOptimization-parameter'
    runAsSystem: true
    runElevated: true
    type: 'PowerShell'
  }
  {
    destination: 'C:\\AVDImage\\multiMediaRedirection.ps1'
    name: 'avdBuiltInScript_multiMediaRedirection'
    sha256Checksum: 'f577c9079aaa7da399121879213825a3f263f7b067951a234509e72f8b59a7fd'
    sourceUri: 'https://raw.githubusercontent.com/Azure/RDS-Templates/master/CustomImageTemplateScripts/CustomImageTemplateScripts_2024-03-27/MultiMediaRedirection.ps1'
    type: 'File'
  }
  {
    inline: [
      'C:\\AVDImage\\multiMediaRedirection.ps1 -VCRedistributableLink "https://aka.ms/vs/17/release/vc_redist.x64.exe" -EnableEdge "true" -EnableChrome "true"'
    ]
    name: 'avdBuiltInScript_multiMediaRedirection-parameter'
    runAsSystem: true
    runElevated: true
    type: 'PowerShell'
  }
  {
    destination: 'C:\\AVDImage\\windowsOptimization.ps1'
    name: 'avdBuiltInScript_windowsOptimization'
    sha256Checksum: '3a84266be0a3fcba89f2adf284f3cc6cc2ac41242921010139d6e9514ead126f'
    sourceUri: 'https://raw.githubusercontent.com/Azure/RDS-Templates/master/CustomImageTemplateScripts/CustomImageTemplateScripts_2024-03-27/WindowsOptimization.ps1'
    type: 'File'
  }
  {
    inline: [
      'C:\\AVDImage\\windowsOptimization.ps1 -Optimizations "DefaultUserSettings"'
    ]
    name: 'avdBuiltInScript_windowsOptimization-parameter'
    runAsSystem: true
    runElevated: true
    type: 'PowerShell'
  }
  {
    name: 'avdBuiltInScript_windowsOptimization-windowsUpdate'
    type: 'WindowsUpdate'
    updateLimit: 0
  }
  {
    name: 'avdBuiltInScript_windowsOptimization-windowsRestart'
    type: 'WindowsRestart'
  }
  {
    name: 'avdBuiltInScript_windowsUpdate'
    type: 'WindowsUpdate'
    updateLimit: 0
  }
  {
    name: 'avdBuiltInScript_windowsUpdate-windowsRestart'
    type: 'WindowsRestart'
  }
  {
    name: 'avdBuiltInScript_adminSysPrep'
    runAsSystem: true
    runElevated: true
    scriptUri: 'https://raw.githubusercontent.com/Azure/RDS-Templates/master/CustomImageTemplateScripts/CustomImageTemplateScripts_2024-03-27/AdminSysPrep.ps1'
    sha256Checksum: '1dcaba4823f9963c9e51c5ce0adce5f546f65ef6034c364ef7325a0451bd9de9'
    type: 'PowerShell'
  }
]

// Output the customizations array
output customizationsOutput array = customize

CustomImageTemplate.bicep

We need to prepare the deployment file for the resources. You need to change the parameters to fit your setup before running the deployment:

param location string = 'westeurope'
param identityName string = 'ManagedIdentityForImageTemplate'
param imageTemplates_name string = 'ImageTemplateForAVD'
param computeGalleryName string = 'MyComputeGallery'
param imageDefinitionName string = 'MyImageDefinition'
param sourceImagePublisher string = 'MyCompanyName'
param sourceImageOffer string = 'MyOfferName' // e.g.: Windows-11-Custom
param sourceImageSku string = 'MySkuName' // e.g.: 24H2
param sourceImageVersion string // This shold be a specific version like '06.07.25' '1.0.0'
param diskSize int = 127
param vmSize string = 'Standard_D2s_v3' //This is not used in the image template, but is required for the image definition

resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = {
  name: identityName
  location: location
}

resource contributor 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = {
  scope: subscription()
  name: 'b24988ac-6180-42a0-ab88-20f7382dd24c'
}

resource rbac 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(identity.id, contributor.id)
  properties: {
    roleDefinitionId: contributor.id
    principalId: identity.properties.principalId
    principalType: 'ServicePrincipal'
  }
}

resource acg 'Microsoft.Compute/galleries@2022-03-03' existing = {
  name: computeGalleryName
}

// Include the customizations module
module customizationsModule 'Customize.bicep' = {
  name: 'customizationsModule'
}

resource imageTemplates_name_resource 'Microsoft.VirtualMachineImages/imageTemplates@2024-02-01' = {
  name: imageTemplates_name
  location: location
  tags: {
    AVD_IMAGE_TEMPLATE: 'AVD_IMAGE_TEMPLATE'
  }
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${identity.id}': {}
    }
  }
  properties: {
    buildTimeoutInMinutes: 240
    customize: customizationsModule.outputs.customizationsOutput
    distribute: [
      {
        artifactTags: {}
        excludeFromLatest: true
        galleryImageId: '${acg.id}/images/${imageDefinitionName}'
        replicationRegions: [
          location
        ]
        runOutputName: 'acgOutput'
        type: 'SharedImage'
      }
    ]
    source: {
      offer: sourceImageOffer
      publisher: sourceImagePublisher
      sku: sourceImageSku
      type: 'PlatformImage'
      version: sourceImageVersion
    }
    vmProfile: {
      osDiskSizeGB: diskSize
      vmSize: vmSize
    }
  }
}

Deployment PowerShell script

This is just an example of how you could deploy the resources. You could also use the default main.bicep with resources. You could also use AZ CLI, and the list of ways to deploy goes on 🙂

This script have I written to show you how to use it in Azure DevOps in a build pipeline with PowerShell. Note that all the input variables must be parsed to the script doing deployment.

$location               = $env:location
$identityName           = $env:identityName
$RG                     = $env:ResourceGroupName
$imageTemplates_name    = $env:imageTemplates_name
$computeGalleryName     = $env:computeGalleryName
$imageDefinitionName    = $env:imageDefinitionName
$sourceImagePublisher   = $env:sourceImagePublisher
$sourceImageOffer       = $env:sourceImageOffer
$sourceImageSku         = $env:sourceImageSku
$sourceImageVersion     = $env:sourceImageVersion
$diskSize               = $env:diskSize
$vmSize                 = $env:vmSize

<#	
	.NOTES
	===========================================================================
     Version:       1.0.0
	 Updated on:   	06-07-2025
	 Created by:   	Christoffer Jakobsen - chkja.dk
	===========================================================================

        PowerShell 7 is required
        Microsoft Azure Accounts Module is required
            Install-Module -Name Az.Accounts
            https://www.powershellgallery.com/packages/Az.Accounts/
        Microsoft Azure KeyVaults Module is required
            Install-Module -Name Az.KeyVault
            https://www.powershellgallery.com/packages/Az.KeyVault/
        Microsoft Azure Desktop Virtulization Module is required
            Install-Module -Name  Az.DesktopVirtualization
            https://www.powershellgallery.com/packages/ Az.DesktopVirtualization/
        
	.DESCRIPTION
		Deploys Custom Image Template for AVD Session Host using Bicep template and parameters file.
#>

#########################################
#                                       #
#             VARIABLES                 #
#                                       #
#########################################
$DevOps = $env:DevOps
$Date = Get-Date -Format "yyyy-MM-dd"

# PATHS
# Define working directory - needed for DevOps mode
if($DevOps -eq "true" -or $DevOps -eq "True")
{
    $DefaultWorkingDirectory = $env:System_DefaultWorkingDirectory+"\"
    write-host "Default Working Directory is:" $DefaultWorkingDirectory
    write-host "DevOps Mode:" $DevOps
}
if($DevOps -ne "true" -or $DevOps -ne "True")
{
    $DefaultWorkingDirectory = "."
    $DevOps = "False"
    write-host "DevOps Mode:" $DevOps
}

#########################################
#                                       #
#             Deployment                #
#                                       #
#########################################

$ARMNameManagedIdentity = "AVD-ManagedIdentity-"+$Date # Name of the deployment in ARM
New-AzResourceGroupDeployment -verbose -Name $ARMNameManagedIdentity `
  -ResourceGroupName $RG `
  -TemplateFile "$DefaultWorkingDirectory\ManagedIdentity.bicep" `
  -location $location `
  -identityName $identityName


$ARMNameCusImgTem = "AVD-CustomImageTemplate-"+$imageTemplates_name+"-"+$Date # Name of the deployment in ARM
New-AzResourceGroupDeployment -verbose -Name $ARMNameCusImgTem `
  -ResourceGroupName $RG `
  -TemplateFile "$DefaultWorkingDirectory\CustomImageTemplate.bicep" `
  -location $location `
  -identityName $identityName `
  -imageTemplates_name $imageTemplates_name `
  -computeGalleryName $computeGalleryName `
  -imageDefinitionName $imageDefinitionName `
  -sourceImagePublisher $sourceImagePublisher `
  -sourceImageOffer $sourceImageOffer `
  -sourceImageSku $sourceImageSku `
  -sourceImageVersion $sourceImageVersion `
  -diskSize $diskSize `
  -vmSize $vmSize

# Trigger the image build

I use this simple script to run via Azure DevOps every time I need to trigger image build from the custom image builder to the compute gallery image definition:

$RG                  = $env:ResourceGroupName
$imageTemplatesName  = $env:ImageTemplatesName


Install-Module -Name Az.ImageBuilder -AllowClobber -Force -Scope CurrentUser
Import-Module Az.ImageBuilder


Start-AzImageBuilderTemplate -ResourceGroupName $RG -Name $imageTemplatesName -NoWait

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *