Baking an AVD Golden Image with Azure Image Builder
Back in 2018, I wrote about an image factory for VMware Workstation. A customer needed images for Horizon Cloud, and I did not want to maintain them by hand or deploy an ESXi host just for image creation. The solution combined Mikael Nyström's Reference Image Factory with Karsten Bott's vmxtoolkit, using MDT to build the guest and VMware Workstation to export it as an OVF.
The target platform is different now, but the reason for automating the build has not changed. When an image needs rebuilding, I want a defined sequence of steps, not a reference VM whose configuration depends on remembering what was changed last time.
For Azure Virtual Desktop, the corresponding approach in my endpoint-toolkit repository uses Azure VM Image Builder, PowerShell customizers, Bicep and Azure DevOps. In this post I will walk through how those parts fit together and the decisions that still need to be made for each environment.
From a task sequence to an image template
Azure VM Image Builder is a managed service built on Packer. You describe the source image, the customizations to apply and the distribution target. The service creates the temporary build infrastructure and handles the image build and generalization process.
That is a useful replacement for the infrastructure around the old factory, but it does not decide what belongs in your image. Applications, language packs, configuration changes and the tests for them are still your responsibility.
| Earlier approach | AVD approach in this repository |
|---|---|
| VMware Workstation build VM | Temporary AIB build infrastructure |
| MDT build and capture task sequence | PowerShell customizers executed by AIB |
| ImageFactory XML configuration | Pipeline variables and image-template parameters |
| OVF export | An image version distributed to Azure Compute Gallery |
| Deployment of the resulting template | Separate session-host deployment and validation |
Prepare the build inputs first
The Bicep module is
imageTemplate.bicep.
It declares an image-template resource using API 2024-02-01 and
supports Marketplace, managed-image and gallery-image-version sources. For the
gallery output, supply an existing image definition compatible with the source
image's generation and security type.
Before running either pipeline, prepare the following:
- Source and destination. Select the Windows edition and source image version, the target gallery definition, region and replica count. Do not assume that a SKU from an example is available or appropriate in your region.
- Identities and permissions. Configure the pipeline's Azure service connection and AIB's user-assigned managed identity. They have different jobs: deploying the template is not the same as reading artifacts and writing the resulting image. Use the permissions described in the AIB documentation.
- Build artifacts. Package the customizers and configuration
files in the layout expected by the pipeline. This module requires both
artifactsBlobUrlandinlineCustomizerScript; a source SKU and destination gallery alone are not a complete build definition. - Network access. Check access to artifact storage and the services used by application installers, language-pack installation and updates. Bundling one dependency does not make the entire build offline.
- Validation and cost. Define the checks you will run on the resulting image, and account for build compute, storage and replication costs. Gallery versioning is useful; the resources behind it are not free.
Windows 365 is a separate image target
You can reuse parts of the build process for Windows 365, but not simply reuse an AVD multi-session image. Windows 365 requires a compatible single-session, generalized Gen2 image, and its requirements exclude FSLogix components and previously joined or MDM-enrolled images. Review the complete Windows 365 image requirements before selecting the source and customizers.
Choose one of the two build pipelines
The repository provides two image-build implementations:
img-build-custom-image.ymluses the community-maintained AIB Task v2 extension.img-build-bicep-only.ymlusesAzureCLI@2and the Bicep module, without the additional AIB extension. The YAML owns artifact preparation, template deployment, build polling and cleanup.
The v2 extension updates the original Microsoft task's Node 10 implementation to Node 20 and adds support for workload identity federation. There is an important distinction here: OIDC authenticates the Azure service connection. The task still uses time-limited, HTTPS-only SAS URLs for staged artifacts. It is not a SAS-free build merely because the service connection no longer uses a client secret.
Both approaches are starting points. Review the service connection, storage, application list, image settings and cleanup behavior before using either one against your own subscription.
Review the customizer sequence
The customizer README documents the sequence used by the supplied pipelines. At the revision reviewed for this post, it is:
DisableAutoUpdates.ps1to control Store auto-downloads, Content Delivery Manager and the Windows Update scheduled-start task during the bake.TimezoneRedirection.ps1.InstallLanguagePacks.ps1, if needed. It is commented out by default in the supplied pipelines.RemoveAppxPackages.ps1, using a package selection you have reviewed.UpdateWinGet.ps1to provision WinGet and install the selected applications.WindowsOptimization.ps1, with the selected optimization groups.UpdateWinGet.ps1 -SkipApps -SkipUserRegistration, a provision-only pass after the optimization step.RemoveUserApps.ps1for the final per-user AppX cleanup.AdminSysPrep.ps1, the final script in this customizer sequence.
The Bicep module also enables a Windows Update customizer by default after this script sequence, before capture. That planned update phase is separate from the background update behavior controlled during the bake. Review its outcome and record the final installed versions too.
The second WinGet pass is deliberate: the optimization step can affect
AppX provisioning. InstallProvisionedAppxPackage.ps1 is optional
and is not wired into either pipeline by default. If required, the documented
insertion point is after package removal and before optimization. Also check
each script's required execution context; administrator and SYSTEM contexts
are not interchangeable for every AppX operation.
For optimization, the warning from my
earlier Windows
optimization post still applies: there is no single list of settings that is
right for every environment. Review what will be disabled and test the applications
that depend on it. -Optimizations All is not a substitute for that review.
The wrapper uses bundled VDOT configuration JSON by default.
That removes the runtime GitHub download for those configuration files, provided
the folder is included in the build artifacts. WinGet, Windows Update and other
installers may still need network access. Likewise, keeping scripts in Git does
not freeze a Marketplace source set to latest or the versions returned
by an application repository. Record those inputs if you need to explain exactly
what went into a particular image.
AdminSysPrep.ps1 patches the expected Sysprep command in
C:\DeprovisioningScript.ps1 to use /quit /mode:vm.
This is a helper for this build recipe, not a general explanation for every
Sysprep failure. Check its output and the capture logs rather than assuming that
running it proves the image is ready.
Plan the deployed update configuration
The repository's ResetAutoUpdateSettings.ps1 is a post-deployment
helper, not part of the current bake chain. Decide which bake-time restrictions
must be reverted and which settings your management policies will own. A successful
capture is not a reason to leave deployed hosts without a working update process.
Creating the template is not running the build
The module defaults autoRun to false. The pipelines
handle starting and monitoring the build, but if you deploy the module manually,
you must also invoke its Run action. The following PowerShell
example uses Azure CLI against an already configured template. Replace
the two example names and select the intended subscription first. Running it
starts a build and incurs Azure resource costs.
$templateId = az resource show `
--resource-group 'rg-avd-imaging' `
--name 'avd-image-template' `
--resource-type 'Microsoft.VirtualMachineImages/imageTemplates' `
--api-version '2024-02-01' `
--query id --output tsv
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($templateId)) {
throw 'Could not resolve the prepared image template.'
}
az resource invoke-action --ids $templateId `
--action Run --api-version '2024-02-01' --no-wait
if ($LASTEXITCODE -ne 0) {
throw 'The image build request failed.'
}
az resource show --ids $templateId `
--api-version '2024-02-01' `
--query properties.lastRunStatus --output json
--no-wait submits the build without waiting for completion.
The final command queries the current run status; it is not a polling loop
or proof that the build has finished. Do not start another run manually when a
pipeline is already managing that template. For the service workflow and log
locations, see the AIB
overview and the pipeline's own output.
Validate before replacing session hosts
The module exposes optional validation settings, but
enableValidation defaults to false. Simply deploying
the template does not enable an application acceptance test. Add build-time
checks where appropriate, then deploy the resulting image to a test host pool
and verify sign-in, profile loading, application launches and the management
and security agents relevant to your environment.
The separate session-host pipelines provide the deployment and cleanup components. Treat promotion and cleanup as separate decisions: identify the accepted image version, retain the previous version and sufficient host capacity, and define when old hosts may be drained and removed.
Keeping the previous image gives you a recovery option. It does not undo changes to profiles, application data or external services, and an older image alone does not restore hosts you have already deleted. Test the recovery procedure as well as the forward deployment.
The main benefit is the same one I was looking for in 2018: less manual image maintenance and a build process that can be reviewed and repeated. AIB handles much of the infrastructure. The remaining work is making the recipe, its dependencies and the acceptance criteria explicit. That is where I would spend the time before adding more customizers.
The implementation details above were checked against repository revision ae652985. Review the current scripts and prerequisites before adapting the recipe.