Automating Windows Virtual Desktop Image build with Hashicorp Packer

In previous blog posts I’ve been writing a bit about the lack of image management options within WVD has (, so therefore we need to be creative in how to actually handle image management and updating. One of the options that I discuss in my post is the use of Packer.

Packer is essentially is image building tool, which can be used to build WVD hosts or VDI instances with ease. In the simplest usage of packer is that you have an JSON file that determines what the image building process is going to contain.

You can download the binary here –>
ou can also find a lot of extensions (provisioners here)

When running packer it essentially needs to have a JSON file which is the image configuration file. We can also have a variable file which I’ve used in the example below which we also call as part of the build. Packer using something called builders which is essentially the platform which Packer will connect to to build the image. In the example below I’m using Azure azure-arm as the builder.

This is an example of a build JSON file that packer has (Where in this case I have extracted out the sensitive information that packer needs to build an image (Client_ID and Client_Secret is a service principal that you create in Azure) tenant_ID is the Azure AD catalog ID and Subscription_ID is the Azure Subscription. This is contained within a seperate JSON file which I use when running the build command.

"variables": {
"client_id": "",
"client_secret": "",
"tenant_id": "",
"subscription_id": ""

"builders": [{
"type": "azure-arm",

"client_id": "{{user `client_id`}}",
"client_secret": "{{user `client_secret`}}",
"tenant_id": "{{user `tenant_id`}}",
"subscription_id": "{{user `subscription_id`}}",
"managed_image_resource_group_name": "rg-service2-core-image",
"managed_image_name": "wvdmgmt-{{isotime \"200601020304\"}}",

"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
"image_offer": "WindowsServer",
"image_sku": "2019-Datacenter",

"communicator": "winrm",
"winrm_use_ssl": true,
"winrm_insecure": true,
"winrm_timeout": "5m",
"winrm_username": "packer",

"azure_tags": {
"dept": "IT",
"task": "Image deployment"
      "location": "West Europe",
      "vm_size": "Standard_DS2_v2"

Next you have to define a build which is where Packer is going to connect to to start the image building process. Next is exentially define a resource group and name of the image which you want to be created. Next you also define which base image you want to use from the Azure Marketplace, you can select any supported OS from the list
You can view the list of available images in the marketplace by using the following CLI commands.

az vm image list-publishers –location westus –output table
az vm image list-offers –location westus –publisher MicrosoftWindowsDesktop –output table
az vm image list-skus –location westus –publisher MicrosoftWindowsDesktop –offer Windows-10 -sku

NOTE: I’m using the -{{isotime \”200601020304\”}}” to automatically create an image with the name of the current date.

It should also be noted that the JSON file above creates a Windows Server image, if you want to create a multi-user Windows 10 image you would need to use the following JSON

"os_type": "Windows",
"image_publisher": "MicrosoftWindowsDesktop",
"image_offer": ""Windows-10",
"image_sku": "19h2-evd",

Next you define communicator which is how Packer will interact with the image during the building process. Since packer will setup a virtual machine in Azure with this example and communicate with it to run any scripts which have been specificed in the JSON file.

The build process goes like this

  1. Create a resource group.
  2. Validate and deploy a VM template.
  3. Execute provision – defined by the user; typically shell commands which are covered further down (Which with Windows is using WMI)
  4. Power off and capture the VM.
  5. Delete the resource group.
  6. Delete the temporary VM’s OS disk.

Lastly we have the provisioners which is the logic we want to run inside the operating system as part of a image build.
Now there are loads of different provisioners which we can use, some are built-in and some are community based which we need to download, but just to give some examples.

Now of course the issue is that we might have multiple components, software and such we want to have installed on our golden image. Now Packer supports Files as a provisioniner which can upload files, but transfer files using WinRM is not always beneficial so therefore I came up with another solution.

When you essentially

You can also view the list of built-in provisioners here –>

NB: We also have post-processors which runs after the image is created, this can be useful for instance if we want to build a docker image and push it to a registry or if we build an image locally and want to import it somewhere.

Once we have defined the logic for the provisioners we can run the build process. This is essentially by running the command Packer.exe build -var-file=var.json image.json (name of the configuration file) on Linux) but if on Windows you need to use the .\Packer.exe -var-file var.json image.json  (if you have any variables) or you can use Packer validate image.json to validate the JSON file.

The var.json file which contains the information about the subscription and such can be configured like this.


    "client_id": "xxx",
    "client_secret": "xxx",
    "tenant_id": "xxx",
    "subscription_id": "xxx"

 Once the process is complete we have a managed image which can be referenced using CLI or Using Terraform.
az vm create \ 
 --resource-group myResourceGroup \ 
 --name myVM \
 --image myPackerImage \ 
 --admin-username azureuser \

Using Terraform to create a VM based upon the same image.

resource "azurerm_image" "test" {
  name = "myPackerImage"
  location = "East US"
  resource_group_name = "myPackerGroup"
  os_disk {
     os_type ="Windows"
     os_state = "Generalized"
     caching = "ReadWrite"

storage_image_reference {
   id = "${}"
This can then be used to create a VM based upon the image created in Azure. It should be noted however that in order to register the VM’s against WVD you would need to install the WVD agents which are referenced here –>

Leave a Reply

Scroll to Top