Table of Contents
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