Building Hyper-V Labs With Packer And Vagrant - Part 1
Posted on June 08, 2021
This two part series will cover the process to automate many of the repetitive tasks we need to perform when building simple lab environments. Labs are one of the best ways to learn and explore but I’ve been finding myself growing more reluctant to spin them up as it always took a couple of hours to get the environment up and running and patched.
In the first part we’re going to cover using Packer to create templates (or golden images) of Windows Server 2019. It will serve as a bit of a Packer primer, and will give you the tools to create your own templates for Windows based operating systems.
The second part will cover using Vagrant to create our lab environment using the Packer images.
Overview
Our template creation process is going to cover the following steps.
- Initial setup: downloading packer, getting ISOs, etc.
- Creating a Hyper-V VM with Packer and automating deployment of Windows Server 2019
- Performing base configuration within our Windows Server VM and installing updates
- Running sysprep to generalize the image and make it suitable for use as a template
Note on security
This guide is meant specifically for ephemeral lab environments that are not internet facing. Credentials are hardcoded and in plain text. The focus is on simplicity, this is not a suitable practice for production workloads.
Initial Setup
With that out the way let’s cover some initial setup.
Hyper-V Install
Firstly you’ll need to ensure you have Hyper-V installed on your machine. This guide assumes a PC running Windows 10 on which Hyper-V can be installed with PowerShell.
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All
Other options are documented here.
Downloading Packer
Like many HashiCorp tools Packer is downloadable as a single binary. Place it wherever makes sense to you on your system and add it to the PATH Environmental variable.
You should be able to open a command prompt and type packer -v
and get the version back.
PS C:\> packer -v
1.7.2
Downloading Windows Server 2019 ISO
You’ll also need an ISO for Windows Server 2019. A trial from the Microsoft Evaluation Center will be fine.
That’s all the initial prep work out the way, let’s move onto building our template.
Creating a Windows Server Image With Packer
Firstly let’s create a folder to store our Windows Server 2019 template configuration. On my system this is called ws2019
and all relative paths will be from the root of this folder.
In this folder create a file called hyperv-ws2019.pkr.hcl
. Packer configuration files should all end with the .pkr.hcl
extension. This file will contain instructions for Packer to use when building our template and it is written in HCL (HashiCorp Configuration Language). Here are the contents:
variable "iso_url" {
type = string
}
variable "iso_checksum" {
type = string
}
variable "pstools_path" {
type = string
}
variable "switch_name" {
type = string
default = "Default Switch"
}
source "hyperv-iso" "ws2019" {
boot_command = ["a<enter><wait>a<enter><wait>a<enter><wait>a<enter>"]
boot_wait = "1s"
cpus = 2
disk_size = 61440
enable_secure_boot = true
generation = 2
iso_checksum = var.iso_checksum
iso_url = var.iso_url
memory = 4096
secondary_iso_images = ["./autounattend/Autounattend.iso"]
shutdown_command = "C:\\Windows\\System32\\Sysprep\\sysprep.exe /generalize /shutdown /oobe /unattend:C:\\Windows\\Temp\\sysprep.xml"
switch_name = var.switch_name
vm_name = "ws2019"
communicator = "winrm"
winrm_username = "Administrator"
winrm_password = "Autodeploy1"
winrm_timeout = "1h"
}
build {
sources = ["source.hyperv-iso.ws2019"]
provisioner "file" {
source = "./sysprep/sysprep.xml"
destination = "C:\\Windows\\Temp\\sysprep.xml"
}
provisioner "file" {
source = "../shared/Patching/Scripts/"
destination = "C:\\Users\\Administrator\\Desktop"
}
provisioner "file" {
source = "../shared/Patching/PSWindowsUpdate"
destination = "C:\\Program Files\\WindowsPowerShell\\Modules"
}
provisioner "powershell" {
inline = [
"New-Item C:\\Tools -Type Directory -Force | Out-Null",
"[Environment]::SetEnvironmentVariable('Path', $ENV:Path + ';C:\\Tools\\PSTools\\', 'Machine')"
]
}
provisioner "file" {
source = var.pstools_path
destination = "C:\\Tools"
}
provisioner "powershell" {
scripts = ["./scripts/1.ps1", "./scripts/2.ps1"]
}
provisioner "windows-restart" {
restart_command = "C:\\Users\\Administrator\\Desktop\\Updates.bat"
restart_timeout = "60m"
}
post-processor "vagrant" {
keep_input_artifact = false
output = "ws2019.box"
}
}
There is a bit to cover here, so let’s go through it. There are three distinct sections in this file.
- The variable declarations
- The
source
block, and - The
build
block
Packer Variable declarations
The variable declarations tell Packer about variables we want to use, which are going to be configuration elements that are defined in a separate “variables” file, rather than hardcoded in our Packer configuration. Elements like the path to the ISO file are going to be different on each system so we can pull them out of the main configuration file.
I like to include a copy of PSTools in my lab templates, so there is a variable for where they’re stored on the local system which Packer will then copy to the template.
Packer Source Block
The source
block uses the Packer Hyper-V ISO builder. This block will give Packer some key details it will need to build the VM and get Windows Server installed. This includes items like the path to the ISO file (specified as a variable), the VM details such as the amount of memory, disk, and number of CPU cores.
Many of the element’s are self-explanatory but here are a few notes for those that may not be.
The boot_command
and boot_wait
settings tell Packer what keystrokes to send on VM boot and how long to wait before sending them. Why is this necessary? Because when Hyper-V boots from the ISO it will prompt us to hit a key to enter the installation program. Without this input the VM would fail to enter the
The secondary_iso_images
setting specifies the location of a second ISO file. The first is the Windows installation media, and the second will contain the Unattended.xml
file which the Windows Server installer uses to automate the installation.
The shutdown_command
specifies the command Packer will run once all build tasks have been completed and the VM is ready to be shut down. Here we’re running sysprep which will set the image to an ‘out of box’ state which will make it suitable for use as a template. We also specify the path of a sysprep.xml
file that will automate the out of box experience once the new image is used to create VMs. This avoids us having to do things like specify a password, accept EULAs, etc.
The switch_name
specifies to which Hyper-V switch the VM will be connected to. The default is ‘Default Switch’, but I tend to create a new External switch and use that instead.
Packer Build Block
Moving down to the build
block, here we tell Packer what steps it needs to perform. We first specify the source
, followed by several provisioners. The source will build our VM, and the provisioners will carry out various tasks. Once all provisioners are done the previously configured shutdown_command
will run.
Packer provides various provisioner types, such as file
which is used to copy files and folders to the VM, powershell
, which runs specified powershell commands or scripts, etc. A list of included provisioners is documented here.
Let’s cover some of the tasks we’re asking Packer to perform.
The first three file
provisioners are copying files over to the new VM. We begin by copying the sysprep.xml
file which will automate the out of box experience when the template is used to create lab VMs, followed by copying some scripts I’ve written to automate the patching process. I’ve included a copy of the PSWindowsUpdate
module written by Michał J. Gajda.
We then use the PowerShell provisioner to create a directory - C:\Tools\
which is where PSTools will reside, and we add C:\Tools\PSTools\
to the System level PATH Environment Variable.
The file provisioner is used again to copy PSTools to the VM, followed by another PowerShell provisioner that runs two scripts. These scripts are currently empty, but I wanted to include them for future use.
Next we need to patch our VM. There are two schools of thought on this, we can patch during the template creation process, or we can patch when we spin up our lab VMs for whatever lab we’re doing. My goal is to get a lab running as quickly as possible, and since we’re automating the creation of the golden image there is nothing stopping us scheduling a task to rebuild the image every month after Patch Tuesday, so with that im mind my preference is to patch during template creation.
Normally the build process would fail if Packer loses WinRM connectivity with VM, but we’re using the windows-restart
provisioner which tells Packer to expect loss of connectivity. However, instead of a reboot command, we’re running our update scripts, which disable WinRM, run the updates including reboots, and finally re-enable WinRM once patching is complete.
The details of how the patching process works are out of scope for this post, but in short we’re using the PSWindowsUpdate
module to deploy updates. The script sets up Autologon using the Administrator account to avoid UAC prompts and places it self in the Startup
folder. When there are no more updates to install, the Autologon registry keys are removed, and a final reboot takes place.
Once updates finish all build tasks complete and the VM is shut down using the sysprep command.
Lastly, we use the Vagrant post processor to convert our Hyper-V image to a Vagrant box resource. If this step is skipped we would be left with a working Hyper-V image that could be used to manually deploy lab machines, but we want to automate that process as well so we’re doing the conversion to file that Vagrant can work with.