Home / Writing / Automation

Baking an AVD Golden Image with Azure Image Builder

2026.07.04 · Automation · Updated 2026-09-18
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.

The image-factory components
Earlier approachAVD approach in this repository
VMware Workstation build VMTemporary AIB build infrastructure
MDT build and capture task sequencePowerShell customizers executed by AIB
ImageFactory XML configurationPipeline variables and image-template parameters
OVF exportAn image version distributed to Azure Compute Gallery
Deployment of the resulting templateSeparate 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:

  1. 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.
  2. 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.
  3. Build artifacts. Package the customizers and configuration files in the layout expected by the pipeline. This module requires both artifactsBlobUrl and inlineCustomizerScript; a source SKU and destination gallery alone are not a complete build definition.
  4. 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.
  5. 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.yml uses the community-maintained AIB Task v2 extension.
  • img-build-bicep-only.yml uses AzureCLI@2 and 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:

  1. DisableAutoUpdates.ps1 to control Store auto-downloads, Content Delivery Manager and the Windows Update scheduled-start task during the bake.
  2. TimezoneRedirection.ps1.
  3. InstallLanguagePacks.ps1, if needed. It is commented out by default in the supplied pipelines.
  4. RemoveAppxPackages.ps1, using a package selection you have reviewed.
  5. UpdateWinGet.ps1 to provision WinGet and install the selected applications.
  6. WindowsOptimization.ps1, with the selected optimization groups.
  7. UpdateWinGet.ps1 -SkipApps -SkipUserRegistration, a provision-only pass after the optimization step.
  8. RemoveUserApps.ps1 for the final per-user AppX cleanup.
  9. 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.