Creating vSphere VM templates with Packer (part 5) - Windows templates

Armed with some vSphere variables, it's now time to create a Windows template. In post 5 of this series that process is described.

Creating vSphere VM templates with Packer (part 5) - Windows templates

It feels like it took a while to get here. In this post though, it's time for the juice! Previous posts have covered the background, terminology and components of Packer. Now it's time to create a template file and get Packer to build.

Template files, as I have mentioned previously, are JSON formatted and define what it is to Packer that you want it to do. These templates can contain a number of elements:

  1. Variables
  2. Builders
  3. Provisioners
  4. Post-Processors (I haven't used any)


I covered variables in the previous two posts. But, you can add variables in to the template files too. You don't have to, they're optional. But putting any template-specific variables at the top of your template file helps others to find the bits that need changing, if they ever do. Let's look at the top of my Windows template file (I'll use Windows 2019 but 2016 is very similar):

  "variables": {
    "var_vm_name_ext":          "2019_vsphere_{{isotime \"20060102_1504\"}}",
    "var_vm_guestos":           "windows9Server64Guest",
    "var_vm_cpu_count":         "1",
    "var_vm_disk_controller":   "pvscsi",
    "var_vm_disk_size":         "51200",
    "var_vm_notes":             "1.0-{{isotime \"20060102_1504\"}} (en_windows_server_2019_updated_feb_2020_x64_dvd_de383770.iso)",
    "var_iso_path":             "os/microsoft/server/2019/en_windows_server_2019_updated_feb_2020_x64_dvd_de383770.iso",
    "var_guest_username":       "administrator",
    "var_guest_password":       "yourpassword"
Variables section of Windows 2019 template JSON

Let me explain these:

  • var_vm_name_ext - The VM needs a name in vCenter. I wanted the build date added in to the name of the VM. The "isotime" function evaluates the current date / time based on golang formatting. This name "extension" variable value is added to the VM name in the Builders section of the template.
  • var_vm_guestos - This is the guest OS identifier in vSphere. When you select your OS type and version in vCenter, this is the value that gets set on the VM. The list of possible values is documented here.
  • var_vm_cpu_count - This is the number of vCPUs the VM should be configured with. I configured my VMs small so that they can be scaled up if required.
  • var_vm_disk_controller - This is the type of disk controller that should be configured. It's not the standard for Windows VMs in vSphere but I prefer it. However, it does mean that a driver is needed to install Windows. More on that later.
  • var_vm_disk_size - This is the size of the system disk (in MB) that I want my VMs to be created with.
  • var_vm_notes - This is the value that I want written in to the VM's annotations field. Again it includes the date / time. This will show up when the template is viewed in vCenter.
  • var_iso_path - The relative path to the ISO file from which I want to install. This will be appended later to the ISO datastore that I defined in my vSphere variables in the previous post.
  • var_guest_username - The user name of the user that Packer will use to connect to the VM after the OS is installed and it first boots.
  • var_guest_password - The password for the user above. These credentials must match what is configured in the unattended answer file for Windows.


Next in the template file is the builders definition. Multiple builders can be included in a single template file. Each builder maps exactly to one VM. In my Windows 2019 template I have two builders, one for Windows 2019 Standard and one for Windows 2019 Standard Core. The two are virtually identical so let's just look at one of them.

"builders": [
      "type":                   "vsphere-iso",
      "name":                   "WindowsServer2019-STANDARD",
      "vcenter_server":         "{{user `var_vsphere_vcenter`}}",
      "username":               "{{user `var_vsphere_username`}}",
      "password":               "{{user `var_vsphere_password`}}",
      "insecure_connection":    "true",
      "datacenter":             "{{user `var_vsphere_datacenter`}}",
      "cluster":                "{{user `var_vsphere_cluster`}}",
      "folder":                 "{{user `var_vsphere_folder`}}",
      "datastore":              "{{user `var_vsphere_datastore`}}",
      "network":                "{{user `var_vsphere_network`}}",
      "convert_to_template":    "true",
      "vm_name":                "ws{{user `var_vm_name_ext`}}",
      "guest_os_type":          "{{user `var_vm_guestos`}}",
      "CPUs":                   "{{user `var_vm_cpu_count`}}",
      "RAM":                    "4096",
      "disk_controller_type":   "{{user `var_vm_disk_controller`}}",
      "disk_size":              "{{user `var_vm_disk_size`}}",
      "disk_thin_provisioned":  true,
      "network_card":           "vmxnet3",
      "notes":                  "{{user `var_vm_notes`}}",
      "communicator":           "winrm",
      "winrm_username":         "{{user `var_guest_username`}}",
      "winrm_password":         "{{user `var_guest_password`}}",
      "iso_paths": [
                                "{{user `var_vsphere_iso_datastore`}} {{user `var_iso_path`}}",
                                "[] /vmimages/tools-isoimages/windows.iso"
      "shutdown_command": "shutdown /s /t 10 /f /d p:4:1 /c Packer_Provisioning_Shutdown",
      "floppy_files": [
      "remove_cdrom":           "true"

Ready for this? Let me explain what these settings all do... (Note that most of the entries pull in values from the various vSphere and template-specific variables that I created. Given that there's two of these builder definitions in my Windows template, you can see why it made sense to use variables I hope!)

  • type - Remember that I said builders were like plugins? The type tells Packer what builder to use. In this case it's the vSphere ISO builder. There are many others available.
  • name - Because I have two builders in this template that are both of type "vsphere-iso" I have to differentiate them. Each one has a unique name, in this case it's "WindowsServer2019-STANDARD".
  • vcenter_server to convert_to_template - These the settings that tell the builder how to connect to vCenter and where to provision the VM.
  • vm_name - This is how the VM object gets named in vCenter. The "extension" I defined above is added to "ws" to produce the name.
  • guest_os_type to notes - These are VM-specific hardware settings, hopefully all self-explanatory. The network card type I could probably put in a variable too I guess. The RAM I didn't put in a variable because it's different for Windows 2019 Standard Core. There it's 2GB instead of 4GB.
  • communicator - This is the mechanism by which Packer will connect to the VM when I define my provisioners. The username and password below are the credentials it will use.
  • iso_paths - There are two entries. The first is the OS installation ISO. The second is to the VMtools ISO on the ESXi host. That is used later to install VMtools. It means that the Windows ISO will be mounted on the VM's D:\ drive and VMtools will be mounted on the VM's E:\ drive.
  • shutdown_command - This is the command that Packer issues to the OS in the VM when the build is complete and all Provisioners have successfully executed. It shuts down Windows.
  • floppy_files - I'll explain that below.
  • remove_cdrom - This seemingly undocumented option removes the CD drives from the VM after it has shutdown. For the most part I doubt that my Windows VMs will need CD drives.

All of that is repeated again for Windows 2019 Standard Core build with the only differences being the VM's name, the amount of RAM allocated and the Autounattend.xml file used on the floppy disk.

On that topic, let me explain the floppy files. To perform an unattended / scripted installation of a Windows OS, you need to give it a configuration file that replaces all of the mouse clicks and key presses that you'd otherwise have to make. You could bake such a file in to your own update version of the ISO file (I think it's possible) but you'd have to do that everytime a new ISO came out and making changes would be time-consuming. For the duration of the install however, having a floppy disk image mounted containing that XML file is something that Packer can automate very easily. The floppy_files option tells Packer which files you want it to combine in to a floppy disk image and present to the VM.

The folder structure where files are taken from to form a floppy disk image

Aside from the Autounattend.xml file I've also included the four files from the pvscsi-win8 folder. These are the driver files for the ParaVirtual Disk Controller. Without these Windows would not find a disk to install on! Also present is a small script to trigger a silent installation of VMtools. Until that's done Windows won't pick up the vmxnet3 network card and the VM won't be able to connect to anything. Finally, phase1-config.ps1. This is a script that I've put there that performs some initial configuration of the VM's OS (kudos to Mark Brookfield for the source). Both this and the VMtools command are triggered as part of the Windows deployment. This is the content for the PS script:

All it does is enable WinRM (Windows Remote Management) so that Packer can connect to execute the Provisioners. Speaking of which...


This is the last section of my Windows template for Packer. I have defined three provisioners. Each type has different options:

"provisioners": [
      "type":                   "powershell",
      "scripts":                ["./windows/scripts/2019/phase2-config.ps1"]
      "type":                   "windows-restart",
      "restart_timeout":        "30m"
      "type":                   "windows-update",
      "search_criteria":        "IsInstalled=0",
      "filters": [
                                "exclude:$_.Title -like '*Preview*'",
Provisioner definition for Windows 2019
  1. This executes my phase 2 configuration script. I'll pop that in below.
  2. Restarts Windows just to make sure that there aren't any pending operations before the next provisioner kicks in.
  3. I mentioned this one in my second post. It's a third-party provisioner that manages the installation of Windows Updates. It's absolutely genius! When executing the output is echoed back to Packer so you can follow along with what it's doing. Below is a quick snippet of Packr running my Windows 2019 template and the Standard Core build hitting the patching provisioner:
The Windows Update provisioner in action

As promised, this is the content of my phase 2 powershell script:

As a summary of what it does:

  • Sets default Explorer view options
  • Disables system hibernation
  • Disables TLS 1.0
  • Disables TLS 1.1
  • Disables password expiration for the local Administrator user
  • Enables RDP connections
  • Installs and trusts my Root and Intermediate CA certificates
  • Installs BGinfo

Per my template configuration, the provisioners use WinRM to connect to the VM and execute. Once they complete successfuly, the same mechanis is used to send the defined shutdown command to the VM.


Once Packer detects that the VM is powered off, the final steps are to remove the floppy and CD drives and convert the VM in to a template. And here it is:

In the next post I'll cover off my CentOS template. Some of it is the same, but there are some differences that need covering.

  1. Introduction
  2. Configuring Packer server and required files
  3. Variables, builders and provisioners
  4. vSphere variables
  5. Windows Server templates <-- $this
  6. CentOS templates
  7. Further plans (including scheduling packer)

My public Packer repository:

Packer templates used in my v12n cloud. Contribute to mpoore/packer development by creating an account on GitHub.