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:
- First we will build our Windows server Vagrant box file with Packer
- We will add that box to Vagrant
- We'll then initialize it with our Vagrantfile template
- 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.
Subscribe to our blog via email
Email subscriptions come from our Atom feed and are handled by Blogtrottr. You will only receive notifications of blog posts, and can unsubscribe any time.
Do you like this blog post and need help with Next Generation Software Engineering, Platform Engineering or Blockchain & Smart Contracts? Contact us.