Devops

Devops

Devops

Nov 28, 2019

Using Packer for building Windows Server Images

Using Packer for building Windows Server Images

Using Packer for building Windows Server Images

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:WindowsTemppuppet-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.