Packer is a useful tool for creating pre-built machine images. While it's usually associated with creating Linux images for a variety of platforms, it also has first class support for Windows.

We'd like to explain why someone should consider adding Packer made images and dive into the variety of ways it benefits a Windows server DevOps environment.

Motivation

Pre-built images are useful in a number of ways. Packer can use the same build configuration and provisioning recipe to create AWS AMI's and Azure machine images that will be used in production, as well as the machine images for testing locally in Virtualbox and Vagrant. This allows teams to develop and test their code using the same setup running in production, as well as the setup their colleagues are using.

In this kind of a setup, you use Packer early in your development process. We follow the workflow where we create the image first, then at any point in the future the image is available for development and deployments. This shifts the work that goes into installing software and configuring an image to long before your deployment time. Therefore there's one less step at deployment and the Windows server image will come out of the gates fully configured and provisioned with the correct software and settings.

Using a pre-built image also has the added benefit that we're able to catch configuration and setup bugs early during the machine creation phase. Any errors which would have occurred during deployment are caught early while we're creating our Windows server image. We'll be confident that our pre-built Windows server image will be ready at the time of deployment.

This could be handy in any number of situations. Imagine a scenario where we need to install an outside piece of software on our Windows server. Maybe we need to setup our Windows server as a Puppet agent prior to deployment. As part of this we'd like to download the .msi package using a simple Powershell script during setup:

$msi_source = "https://downloads.puppetlabs.com/windows/puppet6/puppet-agent-6.4.2-x64.msi"
$msi_dest   = "C:\Windows\Temp\puppet-agent-6.4.2-x64.msi"
Invoke-WebRequest -Uri $msi_source -OutFile $msi_dest

Any issue downloading and retrieving that piece of software from its vendor could delay our entire Windows server deployment and potentially cause downtime or production errors. This sort of problem could arise for a number of reasons:

  • There's an unexpected issue with the network preventing our server from getting the file
  • The software vendor's site is down
  • There's even a humble typo in our download URL

These sort of DevOps pain points should not be allowed to occur at deployment. If we instead started with a pre-built and pre-configured image for our production Windows servers, we could deploy new servers knowing that they would be safely provisioned and set up to our liking.

What is Packer?

So far we've discussed why an engineer would use pre-built Windows images in their DevOps setup without discussing specific tools and methodology. Let's introduce the Packer tool and why it's such a good fit for this problem space.

Packer is an open-source tool developed by HashiCorp for creating machine images. It's an ideal tool to use for our purposes here where we want to create images for multiple platforms (AWS, Azure, Virtualbox) from one build template file.

At a high level, Packer works by allowing us to define which platform we'd like to create our machine or image for with a builder. There are builders for a variety of platforms and we'll touch on using a few of these in our example.

The next thing that Packer lets us do is use provisioners to define steps we want packer to run. We define these provisioning steps in our packer config file and packer will use them to setup our machine images identically, independent of the platforms we target with our builders.

As we mentioned earlier, Packer has excellent Windows support. We'll touch on using the file provisioner as well as as the powershell provisioner in depth later. For now it's worth knowing that we can use the file provisioner to upload files to the Windows server machines we're building. Likewise we can use the PowerShell provisioner to run Powershell scripts that we have on our host machine (the one we're using to create our Windows server images from) on the Windows server we're building.

The nitty gritty - a real world example

Packer works by using a JSON formatted config file. This config file is also referred to as the Packer build template. You specify the builders and provisioners for Packer that we discussed earlier within this build template.

At this point if you would like to follow along and try the next few steps in this example on your own, you should first install Packer on your machine. The official install guide for Packer is here and if you need to install Vagrant, then please follow the official install guide here. Also, check out the corresponding code repository for this blog post here.

Packer is a mature, well-used tool and there are many excellent templates and examples available for a variety of use cases. For our example we're basing our template code on the Packer Windows templates by Stefan Scherer. The set of templates available in that repository are an excellent resource for getting started. The build template specific to our example is available in its entirety at the code repo associated with this blog, but we'll go over a few of the important details next.

The first thing that we'd like to cover is the builder section. For the Vagrant box builder we're using:

{
  "boot_wait": "2m",
  "communicator": "winrm",
  "cpus": 2,
  "disk_size": "{{user `disk_size`}}",
  "floppy_files": [
    "{{user `autounattend`}}",
    "./scripts/disable-screensaver.ps1",
    "./scripts/disable-winrm.ps1",
    "./scripts/enable-winrm.ps1",
    "./scripts/microsoft-updates.bat",
    "./scripts/win-updates.ps1",
    "./scripts/unattend.xml",
    "./scripts/sysprep.bat"
  ],
  "guest_additions_mode": "disable",
  "guest_os_type": "Windows2016_64",
  "headless": "{{user `headless`}}",
  "iso_checksum": "{{user `iso_checksum`}}",
  "iso_checksum_type": "{{user `iso_checksum_type`}}",
  "iso_url": "{{user `iso_url`}}",
  "memory": 2048,
  "shutdown_command": "a:/sysprep.bat",
  "type": "virtualbox-iso",
  "vm_name": "WindowsServer2019",
  "winrm_username": "vagrant",
  "winrm_password": "vagrant",
  "winrm_timeout": "{{user `winrm_timeout`}}"
}

Here the line:

"{{user `autounattend`}}",

is referring to the autounattend variable from the variables section of the Packer build template file:

"variables": {
    "autounattend": "./answer_files/Autounattend.xml",

When you boot a Windows server installation image (like we're doing here with Packer) you'll typically use the Autounattend.xml to automate installation instructions that the user would normally be prompted for. Here we're mounting this file on the virtual machine using the floppy drive (the floppy_files section). We also use this functionality to load PowerShell scripts onto the virtual machine as well. win-updates.ps1 for example installs the latest updates at the time the Windows server image is created.

We're also going to add additional scripts to run with provisioners. These are in the provisioners section of the packer build template and are independent of any specific platform specified by each of the builders section entries.

The provisioners section in our build template looks like the following:

"provisioners": [
  {
    "execute_command": "{{ .Vars }} cmd /c \"{{ .Path }}\"",
    "scripts": [
      "./scripts/vm-guest-tools.bat",
      "./scripts/enable-rdp.bat"
    ],
    "type": "windows-shell"
  },
  {
    "scripts": [
      "./scripts/debloat-windows.ps1"
    ],
    "type": "powershell"
  },
  {
    "restart_timeout": "{{user `restart_timeout`}}",
    "type": "windows-restart"
  },
  {
    "execute_command": "{{ .Vars }} cmd /c \"{{ .Path }}\"",
    "scripts": [
      "./scripts/pin-powershell.bat",
      "./scripts/set-winrm-automatic.bat",
      "./scripts/uac-enable.bat",
      "./scripts/compile-dotnet-assemblies.bat",
      "./scripts/dis-updates.bat"
    ],
    "type": "windows-shell"
  }
],

We're using both the powershell provisioner as well as the Windows Shell provisioner for older Windows CMD scripts. The reason we're using provisioners to run these scripts instead of placing them in the floppy drive like we did in the builder for the Vagrant box earlier is that these scripts are generic to all platforms we'd like our build template to target. For that reason, we would like these to run regardless of the platforms we're using our build template for.

Creating and running a local Windows server in Vagrant

For running our Windows server locally, the general overview is:

  1. First we will build our Windows server Vagrant box file with Packer
  2. We will add that box to Vagrant
  3. We'll then initialize it with our Vagrantfile template
  4. And finally we'll boot it

Building the Packer box can be done with the packer build command. In our example our Windows server build template is called windows_2019.json so we start the packer build with

packer build windows_2019.json

If we have multiple builders we can tell packer that we would only like to use the virtualbox type with the command:

packer build --only=virtualbox-iso windows_2019.json

(Note the type value we set earlier in our Vagrant box builder section of the packer build template was: "type": "virtualbox-iso",).

Next, we'll add the box to vagrant with the vagrant box add command which is used in the following way:

vagrant box add BOX_NAME BOX_FILE

Or more precisely for our example we're invoking this command as:

vagrant box add windows_2019_virtualbox windows_2019_virtualbox.box

We then need to initialize our

vagrant init --template vagrantfile-windows_2019.template windows_2019_virtualbox

and boot it with:

vagrant up

At this point we will have a fully provisioned and running Windows server in Vagrant.

The set of commands we used above to build and use our Packer build template are neatly encapsulated in the Makefile targets. If you're using the example code in the accompanying repo for this blog post you can simply run the following make commands:

make packer-build-box
make vagrant-add-box
make vagrant-init
make vagrant-up

Conclusion

At this point even though we're only going to be using this Vagrant box and its associated Vagrantfile for local testing purposes, we've eliminated the potential for errors that could occur during our Windows server setup. When we use this box for future development and testing (or give it to other colleagues to do likewise) we won't need to be worried that one of our setup scripts may fail and we would need to fix it in order to continue working. We've been able to eliminate an entire category of DevOps errors and a particular development pain point by using Packer to create our Windows server image.

We also know that if we're able to build our box with Packer, and run the provisioning steps, that we'll have a image that will be identical to our production images that we can use to test and work with.

Next steps

If this blog post sounded interesting, or you're curious about other ways modern DevOps tools like Packer can improve your projects, you should check out our future blog posts. We have a series coming soon on using tools, like Packer, to improve your DevOps environment.

In future posts we'll cover ways to use Vagrant with Packer as well as how to use Packer to produce AWS AMI's to deploy your production environment. These will be natural next steps if you wanted to pursue the topics covered in this post further.

We're also adding new DevOps posts all the time and you can sign up for our DevOps mailing list if you would like our latest DevOps articles delivered to your inbox.