Skip to content

dstamand-msft/DevBoxImageBuilder

Repository files navigation

Azure Image Builder for Dev Box

Bicep PowerShell License

OverviewPrerequisitesGetting StartedDeploymentDebugging

An end-to-end solution for creating and managing custom VM images for Microsoft Dev Box using Azure Image Builder and Bicep. Deploy with a single command — or automate via CI/CD — to produce golden images stored in an Azure Compute Gallery.

Overview

Architecture

The solution provisions an Azure Image Builder template that:

  1. Downloads artifacts from a storage account using a managed identity
  2. Runs customization scripts (Entrypoint.ps1) to install software, apply settings, etc.
  3. Cleans up (Exitpoint.ps1) and deprovisions the VM via Sysprep
  4. Distributes the resulting image to an Azure Compute Gallery, with optional multi-region replication

Two Bicep modules handle image template creation:

Module Use case
aib.module.bicep Public networking — no VNet integration
aib.module-private.bicep Private networking — build VM retrieves scripts via private endpoint

Features

  • Infrastructure as Code — fully declarative Bicep templates with parameterized configuration
  • Three networking modes — public, bring-your-own VNet, or fully provisioned VNet (NSGs, NAT gateways, Bastion, private endpoints)
  • Multiple deployment options — Azure DevOps Pipelines, GitHub Actions, Azure Automation, or manual PowerShell
  • Smart redeployment — detects code changes to avoid unnecessary template redeployments
  • Key Vault integration — optionally pass secrets to customization scripts at build time
  • Image replication — distribute images to multiple Azure regions
  • Staging resource group — isolate build-time resources with automatic cleanup

Prerequisites

Azure subscription

Resource providers

Register the following resource providers on your subscription:

  • Microsoft.VirtualMachineImages
  • Microsoft.ContainerInstance

Azure CLI:

az provider register --namespace Microsoft.VirtualMachineImages
az provider register --namespace Microsoft.ContainerInstance

Azure PowerShell:

Register-AzResourceProvider -ProviderNamespace Microsoft.VirtualMachineImages
Register-AzResourceProvider -ProviderNamespace Microsoft.ContainerInstance
RBAC and permissions
Requirement Details
Image distribution The AIB identity needs permissions to distribute images on the Compute Gallery
Managed Identity Operator The AIB identity needs Managed Identity Operator (or Microsoft.ManagedIdentity/userAssignedIdentities/assign/action) on the build VM's user-assigned identity. See the documentation
Staging resource group If using a staging resource group, the Owner role must be assigned to the AIB identity
VNet integration (optional)

When providing your own subnet, ensure:

  1. The AIB managed identity has these VNet permissions:

    • Microsoft.Network/virtualNetworks/read
    • Microsoft.Network/virtualNetworks/subnets/join/action
  2. The Private Link Service network policy is disabled on the subnet. See the documentation:

    Azure CLI:

    az network vnet subnet update \
      --name <subnet_name> \
      --vnet-name <vnet_name> \
      --resource-group <resource_group> \
      --disable-private-link-service-network-policies true

    Azure PowerShell:

    $subnet = '<subnet_name>'
    $net = @{
        Name = '<vnet_name>'
        ResourceGroupName = '<resource_group>'
    }
    $vnet = Get-AzVirtualNetwork @net
    ($vnet | Select -ExpandProperty subnets | Where-Object {$_.Name -eq $subnet}).privateLinkServiceNetworkPolicies = "Disabled"
    $vnet | Set-AzVirtualNetwork

Tools

Getting started

Networking modes

The IaC/ProvisionAll/aib.bicep template supports three networking modes, automatically inferred from the subnetId and virtualNetworkName parameters:

Mode subnetId virtualNetworkName Behavior
Public (empty) (empty) No VNet. Uses the public storage module
Bring-your-own VNet provided (ignored) Your subnet IDs are passed to the private storage module. Networking module is not deployed
Provision VNet (empty) provided Full networking stack is deployed: VNet, subnets, NAT gateways, NSGs, Bastion, and storage private endpoint

Note

When subnetId is provided, virtualNetworkName is ignored — bring-your-own VNet always takes precedence.

The bastionSkuName parameter controls the Azure Bastion SKU (Basic, Standard, or Developer). When set to Developer, the NSG rule on VMBuilderSubnet automatically adds 168.63.129.16/32 as a source, since Developer SKU Bastion connects from the Azure platform IP instead of the Bastion subnet.

Warning

The Developer SKU is a free-tier offering intended for dev/test scenarios only. It does not provide an SLA, does not require a public IP or dedicated AzureBastionSubnet, and must not be used in production. See the Azure Bastion SKU comparison for details.

Choosing a source image

Dev Box requires compatible images. List available base images:

Azure CLI:

az devcenter admin image list --dev-center-name <dev_center_name> --resource-group <resource_group> --query "[].name"

Azure PowerShell:

Get-AzDevCenterAdminImage -DevCenterName <dev_center_name> -ResourceGroupName <resource_group> | Select-Object -ExpandProperty Name

Translate a Dev Box image name to an ImageTemplateSource object. For example, microsoftwindowsdesktop_windows-ent-cpc_win11-24h2-ent-cpc becomes:

{
  "type": "PlatformImage",
  "publisher": "MicrosoftWindowsDesktop",
  "offer": "windows-ent-cpc",
  "sku": "win11-24h2-ent-cpc",
  "version": "latest"
}

Tip

Use the HelperScripts/Get-AzImageInfo.ps1 script for an interactive picker that outputs the correct JSON.

Customization scripts

Place your customization logic in Scripts/Entrypoint.ps1 (runs during image build) and Scripts/Exitpoint.ps1 (cleanup before Sysprep). Both scripts receive -SubscriptionId and optionally -KeyVaultName / -KeyVaultSecretName parameters.

See the Scripts/Examples/ folder for ready-to-use examples, including Dev Box post-setup tasks.

Tip

Add the sha256Checksum property to customizers in aib.module.bicep to ensure script integrity:

(Get-FileHash -Path .\Scripts\DownloadArtifacts.ps1 -Algorithm Sha256).Hash

Deployment

Azure DevOps

Use the pipeline definition in Deployment/azure-pipeline.yaml. It automatically detects whether template redeployment is needed before deploying and running the image build.

GitHub Actions

Use the workflow in Deployment/github-action.yaml. Configure the following secrets and variables in your repository:

Type Name
Secret AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_SUBSCRIPTION_ID
Variable RESOURCE_GROUP_NAME, LOCATION

Azure Automation

Use Deployment/AzureAutomation-Runbook.ps1 as a runbook. It downloads the Bicep templates from a storage account, compiles and deploys them, then runs the image template — all using managed identity.

Manual

Bring your own resources

Azure CLI:

az deployment group create \
  --resource-group <resource_group> \
  --template-file ./IaC/BringYourOwnResources/aib.bicep \
  --parameters <path/to/aib-parameters.jsonc> \
  --verbose

Azure PowerShell:

New-AzResourceGroupDeployment `
  -ResourceGroupName <resource_group> `
  -TemplateFile ./IaC/BringYourOwnResources/aib.bicep `
  -TemplateParameterFile <path/to/aib-parameters.jsonc> `
  -Verbose
Full (provision all resources)

Azure CLI:

az deployment sub create \
  --location <location> \
  --name <deployment_name> \
  --template-file ./IaC/ProvisionAll/aib.bicep \
  --parameters <path/to/aib.parameters.json> \
  --verbose

Azure PowerShell:

New-AzDeployment `
  -Location <location> `
  -Name <deployment_name> `
  -TemplateFile ./IaC/ProvisionAll/aib.bicep `
  -TemplateParameterFile <path/to/aib.parameters.json> `
  -Verbose

After deploying the template, trigger the image build with Deployment/Invoke-ImageTemplate.ps1:

./Deployment/Invoke-ImageTemplate.ps1 `
  -ResourceGroupName <resource_group> `
  -ImageTemplateName <image_template_name> `
  -OutputLogs

Debugging

Build logs are stored in the staging resource group's storage account under the packerlogs blob container. Download the log file to review the full build process.

Tip

CMTrace (found under SMSSETUP\Tools after extraction) provides a more readable log viewing experience.

Debug VM

For deeper troubleshooting, you can deploy a standalone Windows VM into the same VMBuilderSubnet used by Image Builder. This lets you RDP into the network (via Bastion) and manually test scripts, verify connectivity to the storage account, or inspect private endpoints.

Deploy using the IaC/DebugVM/main.bicep template:

Note

The admin password must be 12–123 characters long and meet 3 of 4 complexity requirements: lowercase, uppercase, digit, and special character.

Quick generate with PowerShell:

# Exclude ambiguous or problematic symbols by adding them to the filter
$exclude = [char[]]'`"''{}[]|'
$chars = (33..126 | ForEach-Object { [char]$_ }) | Where-Object { $_ -notin $exclude }
$pwd = -join ($chars | Get-Random -Count 16); Write-Host $pwd

Azure CLI:

az deployment group create \
  --resource-group <resource_group> \
  --template-file ./IaC/DebugVM/main.bicep \
  --parameters ./IaC/DebugVM/main.parameters.jsonc \
  --parameters adminPassword=<admin_password> \
  --verbose

Azure PowerShell:

New-AzResourceGroupDeployment `
  -ResourceGroupName <resource_group> `
  -TemplateFile ./IaC/DebugVM/main.bicep `
  -TemplateParameterFile ./IaC/DebugVM/main.parameters.jsonc `
  -adminPassword (Read-Host -AsSecureString 'Admin Password') `
  -Verbose

Important notes

Important

If you modify files referenced in the image template customizers, you must delete and recreate the template. Azure Image Builder copies those files to the staging resource group at provisioning time and does not detect changes. Applies only in public mode.

Warning

Azure Image Builder does not support service endpoints or private endpoints by design. When using private networking, the build VM retrieves scripts through a private endpoint managed by the aib.module-private.bicep module, not through the built-in File customizer.

Warning

When prepopulateStorageWithExampleScripts is set to true, the storage account's public network access remains enabled so the deployment script can upload example files. Additionally, the deploymentScripts resource provisions a temporary storage account (managed by the platform) with allowSharedKeyAccess enabled, because Azure Container Instances (ACI) can only mount file shares via an access key. If you set prepopulateStorageWithExampleScripts to false and use private networking (subnetId or virtualNetworkName), public network access is automatically disabled and the storage account is only accessible via private endpoints.

Project structure

IaC/
├── aib.module.bicep              # Image template (public networking)
├── aib.module-private.bicep      # Image template (private networking)
├── BringYourOwnResources/        # Deploy into existing infra
├── ProvisionAll/                 # Deploy everything from scratch
│   ├── aib.bicep                 # Main orchestrator
│   ├── networking.bicep          # VNet, NSGs, NAT, Bastion, PE
│   ├── associatedresources.module.bicep
│   ├── stagingresources.module.bicep
│   └── shared.bicep
└── DebugVM/                      # Standalone VM for debugging
Scripts/
├── Entrypoint.ps1                # Build-time customizations
├── Exitpoint.ps1                 # Cleanup before Sysprep
├── DownloadArtifacts.ps1         # Managed-identity artifact download
├── DeprovisioningScript.ps1      # Sysprep generalization
└── Examples/                     # Sample customization scripts
Deployment/
├── azure-pipeline.yaml           # Azure DevOps pipeline
├── github-action.yaml            # GitHub Actions workflow
├── AzureAutomation-Runbook.ps1   # Azure Automation runbook
├── Invoke-ImageTemplate.ps1      # Run the image template
└── Get-CodeChanges.ps1           # Detect changes for redeployment
HelperScripts/
├── Get-AzImageInfo.ps1           # Interactive image source picker
└── HelperFunctions.ps1           # Shared utilities

Resources

About

Solution to build a custom image for Dev Box

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors