With Azure VM Image Builder, you can deploy standardized Virtual Machines with custom images. When you have a MEMCM infrastructure to deploy your on-premises endpoints and want to use it for AVD, Azure VM Image Builder does not have enough options. This article describes how to use MEMCM to create an AVD image.
Azure Automation runbook
We can use Azure Automation runbooks to launch scripts and automate actions in Azure or on-premises using a Hybrid Runbook Worker. It is impossible to combine Azure and on-premises activities in one runbook when writing this article. To avoid extra complexity using multiple child runbooks, I decided to use an on-premises server to orchestrate the entire process.
Infrastructure overview
First, I would like to provide an infrastructure overview to understand the scripting better.

On-premises
- MEMCM infrastructure: a Primary Site consisting of a Site Server, Site Database Server, and Distribution Points.
- Scripting server: will be used to run the PowerShell scripts that trigger actions on MEMCM and Azure.
Azure
- Azure Virtual Desktop: the infrastructure needed for Azure Virtual Desktop.
- Storage Account: a storage container that hosts files to create an Azure Virtual Machine.
- Azure Compute Gallery: to save the images and makes them available for deployment.
Prerequisites
Storage Account
The Storage Account hosts files triggered by scripts when the Azure Virtual Machine cannot access on-premises network shares. It contains a PowerShell script (ConfigureWindowsFirewall.ps1) that configures the Windows Firewall of the Azure Virtual Machine.

Generate a Shared Access Signature URL to make the script available while creating an Azure Virtual Machine.

Replace the URL in the configuration file azvm_template.json with the one created in the previous step.
{
"name": "Microsoft.CustomScriptExtension-20210802121001",
"apiVersion": "2015-01-01",
"type": "Microsoft.Resources/deployments",
"properties": {
"mode": "incremental",
"templateLink": {
"uri": "https://catalogartifact.azureedge.net/publicartifactsmigration/Microsoft.CustomScriptExtension-arm.2.0.56/Artifacts/MainTemplate.json"
},
"parameters": {
"vmName": {
"value": "[parameters('virtualMachineName')]"
},
"location": {
"value": "[parameters('location')]"
},
"fileUris": {
"value": "https://wvdimaging.blob.core.windows.net/scripts/ConfigureWindowsFirewall.ps1?sp=r&st=2022-01-07T17:35:54Z&se=2100-01-08T01:35:54Z&spr=https&sv=2021-01-01&sr=b&sig=xxxxxx"
}
}
},
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', parameters('virtualMachineName'))]"
]
}
Code language: JSON / JSON with Comments (json)
Azure Compute Gallery
The Azure Compute Gallery is used to store the images. A Gallery can have multiple Image Definitions with multiple Image Versions.
Scripting server
Will execute scripts and needs access to the MEMCM infrastructure and Azure. These scripts are available on my GitHub page.
Build custom AVD image
We will create an Azure Virtual Machine with Windows 10 Enterprise (multi-session). The MEMCM Task Sequence will do the OS configuration and application installation. After the sysprep, the script will save the image to the Azure Compute Gallery.
Script
The framework contains the main script (New-AVDGoldenImage.ps1), subscripts, and configuration files. Let me explain how it works step by step.
To execute it, use one of the following command lines:
New-AVDGoldenImage.ps1 -OSWindowsVersion W10_21H2 -OSWindowsType MS -OfficeVersion OFFICE2019 -ApplicationGroup UNIVERSAL -AVDType RA
Code language: PowerShell (powershell)
New-AVDGoldenImage.ps1 -OSWindowsVersion W10_21H2 -OSWindowsType MS -OfficeVersion OFFICE365 -ApplicationGroup UNIVERSAL -AVDType RA
Code language: PowerShell (powershell)
New-AVDGoldenImage.ps1 -OSWindowsVersion W10_21H2 -OSWindowsType MS -OfficeVersion OFFICE2019 -ApplicationGroup UNIVERSAL -AVDType RD
Code language: PowerShell (powershell)
New-AVDGoldenImage.ps1 -OSWindowsVersion W10_21H2 -OSWindowsType MS -OfficeVersion OFFICE365 -ApplicationGroup UNIVERSAL -AVDType RD
Code language: PowerShell (powershell)
New-AVDGoldenImage.ps1 -OSWindowsVersion W10_21H2 -OSWindowsType MS -OfficeVersion OFFICE365 -ApplicationGroup FINANCE -AVDType RA
Code language: PowerShell (powershell)
New-AVDGoldenImage.ps1 -OSWindowsVersion W10_21H2 -OSWindowsType SS -OfficeVersion OFFICE2019 -AVDType RD
Code language: PowerShell (powershell)
New-AVDGoldenImage.ps1 -OSWindowsVersion W10_21H2 -OSWindowsType SS -OfficeVersion OFFICE365 -AVDType RD
Code language: PowerShell (powershell)
- New-AVDGoldenImage.ps1: the main script.
- OSWindowsVersion: the deployed version of Windows 10.
- OSWindowsType: the operating system type:
- MS: multi-session: more than one user can log on simultaneously (e.g. Remote Desktop Services).
- SS: single-session: only one user can log on simultaneously (e.g. classic client OS).
- OfficeVersion: the version of Office that gets installed. In this example, Office 2019 and Office 365 are available.
- OFFICE2019
- OFFICE365
- ApplicationGroup: The MEMCM device collection that will deploy all the applications to the image.
- AVDType: the type of user session. It can be Remote Apps or a full desktop.
- RA: only Remote Apps published to the user.
- RD: the user will get a full desktop to work.
Import PowerShell modules
This script uses PowerShell commands from the Azure PowerShell module. You can install this with the following command:
Install-Module -Name Az
Code language: PowerShell (powershell)
I try to put frequently used PowerShell functions in my module. It is generally available in the PowerShell Gallery.
You need this module to make the script work:
Import-Module SDM -Force
Code language: PowerShell (powershell)
And the last one to communicate with MEMCM:
Import-SDMMEMCMModule
Code language: PowerShell (powershell)
Variables
- LogFolderPath: the log files location.
- ConfigFolderPath: the configuration files location.
- AppTempTargetDir: the location where the script will download the application packages before execution.
- AzVMLocation: the Azure Virtual Machine deploy location.
- AzVMSubnetName: the network subnet name assigned to the network adapter.
- AzVMVirtualNetworkID: the virtual network defined in Azure.
- AzVMResourceGroup: the resource group that contains all objects used in this script.
- AzVMOSDiskType: the type of disk used for the Azure Virtual Machine.
- AzVMSize: the Azure Virtual Machine size (except for the disk space, which is 128GB by default).
- AzVMAdminUserName: the username of the local administrator account.
- AzVMAdminPassword: the password of the local administrator account.
- AzVMPatchMode: the type of patching used.
- AzVMEnableHotPatching: if hot patching is used or not.
- AzCG: the name of the Azure Compute Gallery.
- AzVMPrefix: the prefix of the Azure Virtual Machine hostname.
- OnPremADDomainName: the FQDN of the on-premises Active Directory domain.
- ADCredentialJoinDomainUsername: the domain account with permissions to join a computer object to Active Directory.
- ADCredentialJoinDomainPassword: the account’s password with permissions to join a computer object to Active Directory.
- MEMCMSiteServerHostName: the hostname of the MEMCM server.
- MEMCMSiteCode: the MEMCM server side code.
- OnPremDomainNameServer: the hostname of the Active Directory Domain Controller.
- MEMCMOSDDeviceCollections: an array with MEMCM device collections where the Azure Virtual Machine must be a member of to receive the Task Sequences.
- MEMCMOSDTaskSequences: the MEMCM Task Sequences executed on the Azure Virtual Machine.
Some variables are declared based on the parameters of the main script:
Switch ($OSWindowsType) {
"MS" {
$ImageReferenceSKUOSType = "evd"
}
"SS" {
$ImageReferenceSKUOSType = "ent"
}
}
Switch ($OfficeVersion) {
"OFFICE2019" {
$ImageDefinitionOfficeType = "O2019"
}
"OFFICE365" {
$ImageDefinitionOfficeType = "O365"
}
}
Switch ($OSWindowsVersion) {
"W10_20H2" {
$ImageReferencePublisher = "MicrosoftWindowsDesktop"
$ImageReferenceOffer = "Windows-10"
$ImageReferenceSKU = "20h2-$($ImageReferenceSKUOSType)-g2"
$ImageReferenceVersion = "latest"
$OSWindowsVersion = "20H2"
}
"W10_21H2" {
$ImageReferencePublisher = "MicrosoftWindowsDesktop"
$ImageReferenceOffer = "Windows-10"
$ImageReferenceSKU = "21h2-$($ImageReferenceSKUOSType)-g2"
$ImageReferenceVersion = "latest"
$OSWindowsVersion = "21H2"
}
}
Code language: PowerShell (powershell)
Connect to Azure
To automatically log on to Azure, you need to use a service account that does not have Multi-factor Authentication (MFA) enabled.
Import-AzContext -Path "$($ConfigFolderPath)\azcontext.json" | Out-Null
Code language: PowerShell (powershell)
The command line above will authenticate to Azure using a configuration file.
To create this file, sign in with the service account that does not have MFA and run the following command to save the credentials:
Save-AzContext -Path .\azcontext.json
Code language: PowerShell (powershell)
Create Azure Virtual Machine
A unique hostname will be generated and used to create the Azure Virtual Machine.
$AzVMHostname = New-FuncAzVMHostname
Code language: PowerShell (powershell)
Set the log file path to the generated hostname.
$Global:LogFilePath = $LogFolderPath + "\" + (Get-Item $PSCommandPath).Basename + "-$($AzVMHostname).log"
Code language: PowerShell (powershell)
Create an Azure Virtual Machine based on the generated hostname and variables declared at the beginning of the script.
New-FuncAzVM -AzVMHostname $AzVMHostname -ImageReferencePublisher $ImageReferencePublisher -ImageReferenceOffer $ImageReferenceOffer -ImageReferenceSKU $ImageReferenceSKU -ImageReferenceVersion $ImageReferenceVersion
Code language: PowerShell (powershell)
Enable Remote PowerShell to execute commands to the Azure Virtual Machine from the script.
Enable-FuncRemotePS -AzVMHostname $AzVMHostname
Code language: PowerShell (powershell)
Join the Azure Virtual Machine to the on-premises Active Directory domain.
Join-FuncOnPremADDomain -AzVMHostname $AzVMHostname
Code language: PowerShell (powershell)
Because we do not want Group Policies applied to the Azure Virtual Machine, we must add the service account to the local administrators group.
Add-FuncServiceAccountToLocalAdminGroup -AzVMHostname $AzVMHostname
Code language: PowerShell (powershell)
Restart the Azure Virtual Machine to complete the domain join.
Restart-FuncAzVM -AzVMHostname $AzVMHostname
Code language: PowerShell (powershell)
Wait for the Azure Virtual Machine to be up and running again.
Test-SDMComputerConnection -Name $AzVMHostname -Retry 60
Code language: PowerShell (powershell)
Install the MEMCM client.
Install-FuncPSADTApplication -AzVMHostname $AzVMHostname -ApplicationName "Microsoft MEMCM Client" -SourcesPath "\\domain.local\Packages\Microsoft\MEMCM Client"
Code language: PowerShell (powershell)
Wait until the Azure Virtual Machine is registered in MEMCM.
Test-FuncMEMCMDeviceExists -Name $AzVMHostname -Retry 30
Code language: PowerShell (powershell)
Create some variables for the device in MEMCM. My existing Task Sequence will execute steps and actions based on these variables.
Add-SDMMEMCMDeviceVariable -Name $AzVMHostname -Variable "VAR-LOCATION" -Value "CLOUD" -MEMCMServer $MEMCMSiteServerHostName -MEMCMSiteCode $MEMCMSiteCode
Add-SDMMEMCMDeviceVariable -Name $AzVMHostname -Variable "VAR-WINDOWSVERSION" -Value $OSWindowsVersion -MEMCMServer $MEMCMSiteServerHostName -MEMCMSiteCode $MEMCMSiteCode
Add-SDMMEMCMDeviceVariable -Name $AzVMHostname -Variable "VAR-OFFICEVERSION" -Value $OfficeVersion -MEMCMServer $MEMCMSiteServerHostName -MEMCMSiteCode $MEMCMSiteCode
Add-SDMMEMCMDeviceVariable -Name $AzVMHostname -Variable "VAR-BUILD" -Value "AVD" -MEMCMServer $MEMCMSiteServerHostName -MEMCMSiteCode $MEMCMSiteCode
Add-SDMMEMCMDeviceVariable -Name $AzVMHostname -Variable "VAR-AVDType" -Value $AVDType -MEMCMServer $MEMCMSiteServerHostName -MEMCMSiteCode $MEMCMSiteCode
Code language: PowerShell (powershell)
The MEMCM device will be added to device collections to assign the Task Sequences. Two Task Sequences, because I also use them for our physical endpoints (creation of golden images and the deployment).
ForEach ($MEMCMOSDDeviceCollection In $MEMCMOSDDeviceCollections) {
Add-SDMMEMCMDeviceCollectionDirectMembershipRule -Name $AzVMHostname -DeviceCollection $MEMCMOSDDeviceCollection -MEMCMServer $MEMCMSiteServerHostName -MEMCMSiteCode $MEMCMSiteCode
}
Code language: PowerShell (powershell)
Execute the two Task Sequences in sequential order.
ForEach ($MEMCMOSDTaskSequence In $MEMCMOSDTaskSequences) {
Start-FuncTaskSequence -AzVMHostname $AzVMHostname -TaskSequenceName $MEMCMOSDTaskSequence -Retry 500
}
Code language: PowerShell (powershell)
When the parameter ApplicationGroup is used, the MEMCM device is added to the device collection where all required applications are deployed to.
If ($ApplicationGroup) {
Install-FuncMandatoryApplications -AzVMHostname $AzVMHostname -MEMCMDeviceCollection "AVD-APPS-$($ApplicationGroup)" -MEMCMSiteServerHostName $MEMCMSiteServerHostName -MEMCMSiteCode $MEMCMSiteCode
}
Code language: PowerShell (powershell)
The MEMCM client must be uninstalled before sysprep. Otherwise, the AVD hosts will be registered with the same id and overwrite each other. If a MEMCM client is required on AVD, it should be installed after the deployment in Azure.
Uninstall-FuncPSADTApplication -AzVMHostname $AzVMHostname -ApplicationName "Microsoft MEMCM Client" -SourcesPath "\\domain.local\Packages\Microsoft\MEMCM Client"
Code language: PowerShell (powershell)
Restart the Azure Virtual Machine to make sure all pending reboots are gone.
Restart-FuncAzVM -AzVMHostname $AzVMHostname
Code language: PowerShell (powershell)
Wait for the Azure Virtual Machine to be up and running again.
Test-SDMComputerConnection -Name $AzVMHostname -Retry 60
Code language: PowerShell (powershell)
Copy the application log files created by the PSAppDeployToolkit to the log file directory on the scripting server.
$TargetLogFilePath = "$($LogFolderPath)\" + (Get-Item $PSCommandPath).Basename + "\$($AzVMHostname)"
Copy-FuncLogFiles -AzVMHostname $AzVMHostname -SourceLogFilePath "C:\Windows\Logs\Software" -TargetLogFilePath $TargetLogFilePath
Code language: PowerShell (powershell)
On a shared user environment (e.g. Remote Desktop Services), installing the Citrix Optimizer and Virtual Desktop Optimization Tool can boost performance by disabling and removing all unwanted services, log files and apps. Because these optimizers disable some services that MEMCM uses, the installation must occur at the end.
If ($OSWindowsType -eq "MS") {
Install-FuncPSADTApplication -AzVMHostname $AzVMHostname -ApplicationName "Citrix Optimizer" -SourcesPath "\\domain.local\Packages\Citrix\Optimizer\"
Install-FuncPSADTApplication -AzVMHostname $AzVMHostname -ApplicationName "Virtual Desktop Optimization Tool" -SourcesPath "\\domain.local\Packages\The Virtual Desktop Team\Virtual Desktop Optimization Tool"
}
Code language: PowerShell (powershell)
Sysprep the Azure Virtual Machine to prepare it for capture.
Start-FuncSysPrep -AzVMHostname $AzVMHostname
Code language: PowerShell (powershell)
Stop and deallocate the Azure Virtual Machine.
Stop-FuncAzVM -AzVMHostname $AzVMHostname
Code language: PowerShell (powershell)
Create Azure Compute Gallery image
Configure the Azure Virtual Machine to be “generalized” to use this image for multiple AVD hosts.
Set-FuncAzVMGeneralized -AzVMHostname $AzVMHostname
Code language: PowerShell (powershell)
Create images from the Azure Virtual Machine and save them to two Azure Compute Gallery Definitions. This way, we decide which image version is for testing and which is for production.
If ($ApplicationGroup) {
New-FuncAzCGImageVersion -AzVMHostname $AzVMHostname -AzCGImageDefinitionName "AVD-$($OSWindowsType)-$($ImageDefinitionOfficeType)-$($ApplicationGroup)"
}
Else {
New-FuncAzCGImageVersion -AzVMHostname $AzVMHostname -AzCGImageDefinitionName "AVD-$($OSWindowsType)-$($ImageDefinitionOfficeType)"
}
Code language: PowerShell (powershell)
Cleanup
Remove the Azure Virtual Machine (and components), AD computer object, DNS entry, MEMCM device, and AAD computer object (if you have an automated Azure Hybrid Join policy in place).
Remove-FuncAzVM -AzVMHostname $AzVMHostname
Code language: PowerShell (powershell)
Functions
New-FuncAzVMHostname
Creates a hostname that contains a prefix defined in the variable $AzVMPrefix and a random hexadecimal string with four characters. If a host already exists with that hostname, it will generate another one.
New-FuncAzVM
Creates an Azure Virtual Machine based on json configuration files and populates it with variables in the script.
Restart-FuncAzVM
Restarts an Azure Virtual Machine.
Stop-FuncAzVM
Stops and deallocates an Azure Virtual Machine.
Remove-FuncAzVM
Removes the objects that are created during the execution of the script:
- Azure Virtual Machine and its resources (network interface, network security group, and disk)
- On-premises Active Directory computer object
- DNS record
- Azure Active Directory computer object
- MEMCM device
Enable-FuncRemotePS
Enables remote PowerShell on the Azure Virtual Machine to execute remote PowerShell commands.
Join-FuncOnPremADDomain
Joins the Azure Virtual Machine to the on-premises Active Directory domain.
Install-FuncPSADTApplication
Downloads application sources created with PSAppDeployToolkit to the Azure Virtual Machine and installs it locally with the system account.
Uninstall-FuncPSADTApplication
Downloads application sources created with PSAppDeployToolkit to the Azure Virtual Machine and uninstalls it locally with the system account.
Test-FuncMEMCMDeviceExists
Checks whether a MEMCM device exists. When it does not, it will wait for it.
Start-FuncTaskSequence
Starts a MEMCM Task Sequence on an Azure Virtual Machine and waits for it to finish.
Install-FuncMandatoryApplications
Adds the MEMCM device to the device collection defined in the parameter “MEMCMDeviceCollection” (based on variable $ApplicationGroup) where the mandatory applications are deployed to and waits for all applications to be installed.
Resume-FuncInstallMandatoryApplication
When an application installation returns an error, this function will retry it.
Copy-FuncLogFiles
Copies log files from the Azure Virtual Machine to the logging directory on the scripting server.
Start-FuncSysPrep
Starts the sysprep process on the Azure Virtual Machine and shuts it down.
New-FuncAzCGImageVersion
Creates two images in the Azure Compute Gallery. One for user acceptance testing and one for production. The image for user acceptance testing will be set as the “latest version” in the image definition, while the production image definition must be changed manually.
Add-FuncServiceAccountToLocalAdminGroup
Adds the service account to the local administrators group of the Azure Virtual Machine.
Set-FuncAzVMGeneralized
Sets the Azure Virtual Machine to the “generalized” state. This way, the image can be used on multiple hosts.