The Multi-Cloud experts behind the scenes: Using Packer to automate Gold Image creation

Published 10th May 2019 in Blogs

The concept of gold master as an image of an operating system, which is then used as a template to build operating systems for future builds, is one that has been around for a while in the IT industry.

Not having to perform a fresh install of an operating system, patch it and apply security patches each time a new server or service is required is certainly appealing. But in addition, having an operating system as a standard build, which all services are then created from — a gold image — ensures a consistency of build for each server.

Creating gold images is a time saving operation, however it comes with its own set of problems. Applying patches, tweaking settings, updating or adding core software can, on occasion, result in days of work to keep gold images up to date.

Having identified these issues, UKCloud’s Automation & Service Reliability (ASR) team investigated methods of automating the creation of our gold images from which our internal servers are built. Having found many options, we decided to create our master images from code using Packer.

Packer is a solution that enables us, through a Jenkins pipeline, to provide a config file designed to build and setup both Windows and Linux servers. This is from install and patching through to installing team-specific applications onto a server.

This blog takes you behind the scenes and shows an example of how we could utilise Packer with VirtualBox to build Windows 2016 server images. The ASR team are working closely with our Multi-cloud technology teams, and work has already begun to expose a variant of this process in our builds for customers. Look out for future blogs on updates and we will let you know as soon as this is ready for release on our customer platform.

 

What is Packer?

According to HashiCorp (the creators of Packer):

Packer is easy to use and automates the creation of any type of machine image. It embraces modern configuration management by encouraging you to use automated scripts to install and configure the software within your Packer-made images. Packer brings machine images into the modern age, unlocking untapped potential and opening new opportunities.

In other words:

Packer enables you to take OS master image creation to the next level. It means you’re no longer limited to having to manually create gold masters of your favourite operating systems, and enables you to create infrastructure as code.

Packer terminology

There are a handful of terms used throughout the Packer documentation, where the meaning may not be immediately obvious if you aren’t familiar with Packer. We’ve included the definitions of the terminology used by HashiCorp below:

The Packer JSON template

The core of any Packer project is the template. It defines all the base parameters for the build and tells Packer which scripts/processes to run against the image (and how).

An example Packer template looks like this:

{
  "variables": {
    "name": "windows_2016",
    "version": "1.0",
    "iso_url": "<Put your ISO URL here>",
    "iso_checksum_type": "md5",
    "iso_checksum": "<put your ISO MD5 checksum here>",
    "ssh_user": "Administrator",
    "ssh_pass": "<put your desired password for the Administrator account here>",
    "disk_size": "81920",
    "memory": "2048",
    "cpu": "2",
    "output_directory": "./output/windows_2016-base"
  },
  "builders": [
    {
      "type": "virtualbox-iso",
      "skip_export": "false",
      "format": "ova",
      "output_directory": "{{user `output_directory`}}",
      "vm_name": "{{user `name`}}",
      "headless": true,
      "boot_wait": "1m",
      "iso_url": "{{ user `iso_url` }}",
      "iso_checksum": "{{ user `iso_checksum` }}",
      "iso_checksum_type": "{{ user `iso_checksum_type` }}",
      "communicator": "winrm",
      "winrm_username": "{{user `ssh_user`}}",
      "winrm_password": "{{user `ssh_pass`}}",
      "winrm_timeout": "12h",
      "shutdown_command": "C:/Windows/packer/PackerShutdown.bat",
      "shutdown_timeout": "10m",
      "guest_os_type": "Windows2016_64",
      "sound": "none",
      "guest_additions_mode": "attach",
      "disk_size": "{{user `disk_size`}}",
      "cpus": "{{user `cpu`}}",
      "memory": "{{user `memory`}}",
      "floppy_files": [
        "./unattend/windows_2016/autounattend.xml",
        "./floppy/winrm.ps1"
      ]
    }
  ],
  "provisioners": [
    {
      "type": "powershell",
      "script": "scripts/install_oracle_guest_additions.ps1"
    },
    {
      "type": "windows-restart"
    },
    {
      "type": "powershell",
      "script": "scripts/set_wsus_registry.ps1"
    },
    {
      "type": "windows-update"
    },
    {
      "type": "powershell",
      "script": "scripts/install_puppet.ps1"
    },
    {
      "type": "powershell",
      "script": "scripts/set_firewall_rules.ps1"
    },
    {
      "type": "powershell",
      "script": "scripts/cleanup.ps1"
    },
    {
      "type": "windows-restart"
    },
    {
      "type": "powershell",
      "script": "scripts/save_shutdown_command1.ps1"
    }
  ]
}

 

This template file:

To help explain this example further, we’ve broken down the information into the following chunks with a short explanation.

Variables

We define the environment/global variables in the “variables”:{} section.

In the above example template, there are three variables you will want to set (make your changes by replacing the hint text, including the angled brackets):

iso_url – This is the address of the ISO file. You can specify the absolute path of the ISO file (for example, C:\Packer\Test.iso), the relative path of the ISO (for example, ./ISO/Test.iso), or an internally-hosted URL (as there is no public directly-accessible source of the ISO file itself).

iso_checksum – This is the (in this case MD5) checksum of the ISO file presented. If the checksum doesn’t match when the template is run, it will error out.

ssh_pass – This is the password for the Administrator account, which is created by default on all Windows Server installs. Customise this as you require, but remember to be careful if specifying special characters, as the keymap of your region might be different to that specified in the autounattend.xml file (we’ll get to that in a bit).

output_directory – This is the destination of the OVA created during the Packer build run. Between runs, you should delete the directory. Leaving the (to use the example) windows_2016-base directory present will cause Packer to error and not build.

To run this Packer build template, you’ll require a copy of the ISO for Windows Server 2016. If you don’t already have a copy, you can find it here:

https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2016

There are other (hopefully self-explanatory) variables set out in this section. The only things to watch out for are that disk_size and memory are specified in MiB (so 2048 MiB = 2 GiB). version is optional and used for tracking build versions during development. You can set this value to any string, and it will be used by Jenkins/Vagrant.

Builders

We define (in this example) the VirtualBox builder in the “builders”:[] section.

This example uses VirtualBox as it is freeware and supported on almost every platform.

This section mostly reads in the variables that were defined earlier (to make editing easier/more logical).

As this is a Windows build, we’ve specified that we’ll use WinRM (Windows Remote Management) to communicate with the VM. Later on, we’ll explain how this is achieved (as WinRM can be rather tricky to get working due to the intended secure nature of the protocol).

winrm_timeout is specified as 12h (12 hours) to allow for WinRM to respond slowly during the Windows Update stage of the build.

In this example, we’ve specified a special shutdown command. This is run by the target VM at the point when Packer needs to shut down the instance for exporting to the OVA. This file is generated on the VM by a script (explained in detail later on).

floppy_files specifies two files that are copied to a virtual floppy disk that is attached to the VM by VirtualBox. You can specify as many files as you like, provided you stick within the 1.44MiB capacity limit of a 3.5″ HD floppy disk. Larger files (if required) are provisioned in another way (not covered in this example). We’ll go through the autounattend.xml file in brief (as it is quite lengthy) later in this blog.

Provisioners

We define the provisioning scripts in the “provisioners”:[] section.

scripts/install_oracle_guest_additions.ps1

The first script to run installs the VirtualBox Additions to allow for an easier life when working with this VM under VirtualBox. In the Builders section above, we specified guest_additions_mode as attach. This tells Packer to download the latest version of the VirtualBox additions ISO and attaches (mounts) it on the VM. The script we’ve specified as the first provisioner is used to install the Additions package (as Packer itself doesn’t always reliably complete the installation without user assistance).

Code example:

$ErrorActionPreference = 'Stop'
 
 
Write-Output "Installing Virtualbox Guest Additions"
Write-Output "Checking for Certificates in vBox ISO"
if(test-path E:\ -Filter *.cer)
{
  Get-ChildItem E:\cert -Filter *.cer | ForEach-Object { certutil -addstore -f "TrustedPublisher" $_.FullName }
}
Start-Process -FilePath "E:\VBoxWindowsAdditions.exe" -ArgumentList "/S" -Wait

The above PowerShell script installs the Oracle certificates provided on the ISO as trusted certs on the VM, before installing the Additions package. This ensures there are no certificate warnings when it comes to installing the drivers Oracle provides in the package.

windows-restart

This, as you might imagine, initiates a reboot of the Windows VM.

scripts/set_wsus_registry.ps1

This provisioner script is useful for corporate/enterprise environments where a WSUS server (or more than one) is available to speed up Windows Update runs.

In this example, the lines are commented out to allow you to keep this step optional.

To customise this script for your own environment, adjust the lines in the Param () section.

Code example:

<#
.SYNOPSIS
  Configures the local machine for WSUS updates instead of Windows Updates.
 
 
.DESCRIPTION
  This script will create all the registry keys required to ensure that Windows Updates
  come from the specified WSUS server, and not from the internet. Usually, this will later
  be overwritten by Group Policy Objects once the machine is on the domain.
 
.INPUTS
  None.
 
.OUTPUTS
  None.
#>
 
Param (
  # The full URI to the WSUS server to use for this client.
  [String] $wsusServer = "http://wsus.example.local:8530",
 
  # The name of the  WSUS Group in which to place this client.
  [String] $wsusGroup = "Example.WsusGroup"
)
 
# Write-Host "Setting WSUS Registry Entries"
# $regPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"
# New-ItemProperty -Path $regPath -Name "WUServer" -Value $wsusServer -PropertyType String -Force
# New-ItemProperty -Path $regPath -Name "WUStatusServer" -Value $wsusServer -PropertyType String -Force
# New-ItemProperty -Path $regPath -Name "TargetGroupEnabled" -Value 1 -PropertyType DWord -Force
# New-ItemProperty -Path $regPath -Name "TargetGroup" -Value $wsusGroup -PropertyType String -Force
 
 
# $regPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU"
# New-ItemProperty -Path $regPath -Name "NoAutoUpdate" -Value 0 -PropertyType DWord -Force
# New-ItemProperty -Path $regPath -Name "AUOptions" -Value 3 -PropertyType DWord -Force
# New-ItemProperty -Path $regPath -Name "ScheduledInstallDay" -Value 0 -PropertyType DWord -Force
# New-ItemProperty -Path $regPath -Name "ScheduledInstallTime" -Value 3 -PropertyType DWord -Force
# New-ItemProperty -Path $regPath -Name "AutoInstallMinorUpdates" -Value 1 -PropertyType DWord -Force
# New-ItemProperty -Path $regPath -Name "UseWUServer" -Value 1 -PropertyType DWord -Force

Don’t forget to remove the # comment marks at the beginning of the registry change blocks, if you want to utilise this script. Unfortunately, due to the JSON format of the main Packer template, it’s not possible to comment out lines in the template. By commenting out the lines in the provisioner script itself, we achieve the same result.

windows-update

This step initiates a full run of Windows Update, installing all critical and optional updates. If you want to skip this step during development to make testing much faster (running Windows Update on my test laptop adds approximately 2-3 hours to the build time), edit this line to windows-restart instead.

scripts/install_puppet.ps1

You can use this script to download a Puppet Agent installer from a URL of your choice. If you want to address this at a later stage, leave the lower lines (beginning with Write-Host) commented out.

Code example:

<#
.SYNOPSIS
  Installs the Puppet agent.
 
 
.DESCRIPTION
  Installs puppet agent from the internal repositories.
  By default, the agent is disabled and should be enabled when ready as part of deployment.
  This is because the hostname and puppet certs for this machine need to be made unique first.
 
.INPUTS
  None.
 
.OUTPUTS
  None.
#>
 
# Write-Host "Downloading Puppet agent..."
# Invoke-WebRequest -Uri "http://puppet.example.local/bin/puppet/agent/puppet-agent-x64-latest.msi" -OutFile "C:\Windows\Temp\puppet.msi"
# Write-Host "Installing Puppet agent..."
# Start-Process C:\Windows\System32\msiexec.exe -ArgumentList "/qb /i C:\Windows\Temp\puppet.msi /log C:\Windows\Temp\puppet.log" -Wait
# Write-Host "Making sure Puppet agent is disabled by default..."
# Set-Service -Name "puppet" -StartupType disabled
# Write-Host "Done installing Puppet agent."

You should use the URL of your local internal repository instead of the example line (the line that begins with Invoke-WebRequest).

scripts/set_firewall_rules.ps1

This script enables ICMP Ping and RDP. This may be more useful when run as part of a Jenkins pipeline, but is included here as an example of the functionality of Packer.

Code example:

<#
.SYNOPSIS
  Configures the local firewall and access restrictions.
 
 
.DESCRIPTION
  By default the server will prevent incoming ICMP requests. As the Jenkins pipeline uses Ping to
  see if the server is up yet or not, we need to make sure this is enabled.
  Also enables Remote Desktop. In theory this should be set by Puppet or GPO, but it makes deployment checks easier.
 
.INPUTS
  None.
 
.OUTPUTS
  None.
#>
 
 
# Allow incoming ping
Set-NetFirewallRule -DisplayName "File and Printer Sharing (Echo Request - ICMPv4-In)" -Enabled True
 
# Enable Remote Desktop.
(Get-WmiObject Win32_TerminalServiceSetting -Namespace root\cimv2\TerminalServices).SetAllowTsConnections(1,1) | Out-Null
(Get-WmiObject -Class "Win32_TSGeneralSetting" -Namespace root\cimv2\TerminalServices -Filter "TerminalName='RDP-tcp'").SetUserAuthenticationRequired(1) | Out-Null
Set-NetFirewallRule -DisplayName "Remote Desktop*" -Enabled True

scripts/cleanup.ps1

This provisioner script cleans up the machine in preparation for conversion into an OVA. It includes various optional features that you can turn on if desired. But take a note of the time penalties before choosing to enable an option.

Code example:

<#
.SYNOPSIS
  Performs various tasks on this host to prepare it for imaging.
 
 
.DESCRIPTION
  This script will perform various tasks depending on the parameters provided, to cleanup, anonymise and compact the image.
 
.INPUTS
  None.
 
.OUTPUTS
  None.
#>
 
Param (
  # Remove cached update files. Prevents uninstalling updates. Fairly quick.
  [Switch] $CleanUpdates = $true,
 
  # Empty out Windows Event Logs. Fairly quick.
  [Switch] $CleanEventLogs = $true,
 
  # Remove temporary files used during build, such as Temp, Panther, Nuget and logs folders. Fairly quick.
  [Switch] $DeleteBuildfiles = $true,
 
  # Defragment the system drive. Can take a long time so defaults to false.
  [Switch] $DefragDisk = $false,
 
  # Force an optimisation of the SxS cache. Can take a long time so defaults to false.
  [Switch] $CleanSxS = $false,
 
  # Zero out all unused parts of the system disk, which can improve compression of the VMDK. Can take a long time so defaults to false.
  [Switch] $ZeroDisk = $false
)
 
 
# Disable auto-login
$regPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
Set-ItemProperty -Path $regPath -Name "AutoAdminLogon" -Value 0
 
 
# Remove cached update files.
If($CleanUpdates) {
  Write-Host "Cleaning updates.."
  Stop-Service -Name wuauserv -Force
  Remove-Item c:\Windows\SoftwareDistribution\Download\* -Recurse -Force
  Start-Service -Name wuauserv
}
 
 
# Emptying Logs
# This shouldn't really be necessary, as SysPrep should do this too
If($CleanEventLogs) {
  (Get-WinEvent -ListLog *).logname | ForEach-Object {[System.Diagnostics.Eventing.Reader.EventLogSession]::GlobalSession.ClearLog("$psitem")}
}
 
 
# Write-Host "Cleaning SxS..."
If($CleanSxS) {
  Dism.exe /online /Cleanup-Image /StartComponentCleanup /ResetBase
}
 
$directories = @()
$directories += "$env:localappdata\Nuget"
$directories += "$env:localappdata\temp\*"
$directories += "$env:windir\logs"
$directories += "$env:windir\panther"
$directories += "$env:windir\winsxs\manifestcache"
# $directories += "C:\packer" # Don't remove the packer folder any more, we need the unattend.xml
 
If($CleanBuildFiles) {
  $directories | % {
      if(Test-Path $_) {
          Write-Host "Removing $_"
          try {
            Takeown /d Y /R /f $_
            Icacls $_ /GRANT:r administrators:F /T /c /q  2>&1 | Out-Null
            Remove-Item $_ -Recurse -Force | Out-Null
          } catch { $global:error.RemoveAt(0) }
      }
  }
}
 
If($DefragDisk) {
  Write-Host "Defragging..."
  if (Get-Command Optimize-Volume -ErrorAction SilentlyContinue) {
      Optimize-Volume -DriveLetter C
      } else {
      Defrag.exe c: /H
  }
}
 
 
If($ZeroDisk) {
  Write-Host "Zeroing out empty space..."
  $FilePath="c:\zero.tmp"
  $Volume = Get-WmiObject win32_logicaldisk -filter "DeviceID='C:'"
  $ArraySize= 64kb
  $SpaceToLeave= $Volume.Size * 0.05
  $FileSize= $Volume.FreeSpace - $SpacetoLeave
  $ZeroArray= new-object byte[]($ArraySize)
 
  $Stream= [io.File]::OpenWrite($FilePath)
  try {
     $CurFileSize = 0
      while($CurFileSize -lt $FileSize) {
          $Stream.Write($ZeroArray,0, $ZeroArray.Length)
          $CurFileSize +=$ZeroArray.Length
      }
  }
  finally {
      if($Stream) {
          $Stream.Close()
      }
  }
 
  Del $FilePath
}

windows-restart

This step runs a final reboot following the clean-up exercise.

scripts/save_shutdown_command1.ps1

Code example:

$packerWindowsDir = 'C:\Windows\packer'
New-Item -Path $packerWindowsDir -ItemType Directory -Force
 
 
# final shutdown command
$shutdownCmd = @"
netsh advfirewall firewall set rule name="WinRM-HTTP" new action=block
 
C:/windows/system32/sysprep/sysprep.exe /generalize /oobe /unattend:C:/Windows/packer/unattended.xml /quiet /shutdown
"@
 
# unattend XML to run on first boot after sysprep
$unattendedXML = @"
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="generalize">
        <component name="Microsoft-Windows-Security-SPP" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <SkipRearm>0</SkipRearm>
        </component>
        <component name="Microsoft-Windows-PnpSysprep" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <PersistAllDeviceInstalls>true</PersistAllDeviceInstalls>
            <DoNotCleanUpNonPresentDevices>false</DoNotCleanUpNonPresentDevices>
        </component>
    </settings>
    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
          <InputLocale>0809:00000809</InputLocale>
          <SystemLocale>en-GB</SystemLocale>
          <UILanguage>en-US</UILanguage>
          <UserLocale>en-GB</UserLocale>
        </component>
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <OOBE>
                <HideEULAPage>true</HideEULAPage>
                <ProtectYourPC>1</ProtectYourPC>
                <NetworkLocation>Work</NetworkLocation>
                <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
            </OOBE>
            <RegisteredOrganization>ExampleOrg</RegisteredOrganization>
            <RegisteredOwner>ExampleUser</RegisteredOwner>
            <TimeZone>GMT Standard Time</TimeZone>
            <UserAccounts>
                <AdministratorPassword>
                    <Value>example_password</Value>
                    <PlainText>true</PlainText>
                </AdministratorPassword>
            </UserAccounts>
            <AutoLogon>
              <Enabled>false</Enabled>
            </AutoLogon>
        </component>
    </settings>
    <settings pass="specialize">
    </settings>
</unattend>
"@
 
Set-Content -Path "$($packerWindowsDir)\PackerShutdown.bat" -Value $shutdownCmd
Set-Content -Path "$($packerWindowsDir)\unattended.xml" -Value $unattendedXML
 
# will run on first boot
# https://technet.microsoft.com/en-us/library/cc766314(v=ws.10).aspx
$setupComplete = @"
netsh advfirewall firewall set rule name="WinRM-HTTP" new action=allow
"@
 
New-Item -Path 'C:\Windows\Setup\Scripts' -ItemType Directory -Force
Set-Content -path "C:\Windows\Setup\Scripts\SetupComplete.cmd" -Value $setupComplete

This script creates the C:\Windows\packer\PackerShutdown.bat, C:\Windows\packer\unattended.xml and C:\Windows\Setup\Scripts\SetupComplete.cmd files, using the XML and PowerShell script above.

The PackerShutdown.bat file is created with the contents of the $shutdownCmd section of the script. This shuts the WinRM HTTP port on the Windows firewall, which is useful when using Packer in combination with Jenkins for automated deployment runs. The second line of PackerShutdown.bat runs sysprep using the XML presented in the $unattendedXML section of the script. This should be edited to suit your needs, in particular the RegisteredOrganization, RegisteredOwner and AdministratorPassword sections. If you’re in the UK, then the language settings are correct. You must leave UILanguage as en-US, as the Windows installer itself is hardcoded to US English.

The final section of the script creates and sets up a final script to run (after Packer itself has completed). This is defined in $setupComplete in the script, and simply re-opens the WinRM HTTP port on the firewall. This is done to avoid Jenkins (or Terraform) from becoming confused when they see WinRM come up initially, and makes those deployment tools wait until sysprep has been completed first.

This completes the template. We’ll now explain the other files mentioned in the scripts, which are necessary for a successful run of the template.

floppy/winrm.ps1

This script is triggered automatically to run on first boot by autounattend.xml and enables WinRM so that Packer can take control of the VM.

Code example:

Start-Transcript -Path C:\Windows\Temp\winrm.log
 
 
if([environment]::OSVersion.version.Major -ge 6) {
  Write-Host "Environment $([environment]::OSVersion.version.Major) is >6"
 
  # You cannot change the network location if you are joined to a domain, so abort
  if(1,3,4,5 -contains (Get-WmiObject win32_computersystem).DomainRole) { return }
  Write-Host "Domain Role is not 1,3,4,5, so continuing"
 
  # Get network connections
  $networkListManager = [Activator]::CreateInstance([Type]::GetTypeFromCLSID([Guid]"{DCB00C01-570F-4A9B-8D69-199FDBA5723B}"))
  Write-Host "Created network list manager"
 
  $connections = $networkListManager.GetNetworkConnections()
  Write-Host "Got connections from network list manager"
 
  $connections |foreach {
    Write-Host $_.GetNetwork().GetName()"category was previously set to"$_.GetNetwork().GetCategory()
    $_.GetNetwork().SetCategory(1)
    Write-Host $_.GetNetwork().GetName()"changed to category"$_.GetNetwork().GetCategory()
  }
}
 
Enable-PSRemoting -Force
winrm quickconfig -q
 
winrm set winrm/config/client/auth '@{Basic="true"}'
winrm set winrm/config/service/auth '@{Basic="true"}'
winrm set winrm/config '@{MaxTimeoutms="1800000"}'
winrm set winrm/config/service '@{AllowUnencrypted="true"}'
winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="2048"}'
Restart-Service -Name WinRM
netsh advfirewall firewall add rule name="WinRM-HTTP" dir=in localport=5985 protocol=TCP action=allow
netsh advfirewall firewall add rule name="WinRM-HTTP" dir=in localport=5986 protocol=TCP action=allow
 
net stop winrm
sc.exe config winrm start=auto
net start winrm

The settings it forces result in WinRM running in insecure mode temporarily. This will all be overwritten by Group Policy once the machine is joined to a domain, but is necessary to allow smooth creation of the OVA.

unattend/windows_2016/autounattend.xml

This XML file, as it is placed on the root of the A: drive by Packer, is used by the Windows installer automatically.

Code example:

<?xml version="1.0" encoding="UTF-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
  <settings pass="windowsPE">
    <component name="Microsoft-Windows-PnpCustomizationsWinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <DriverPaths>
        <PathAndCredentials wcm:keyValue="1" wcm:action="add">
          <Path>A:\</Path>
        </PathAndCredentials>
      </DriverPaths>
    </component>
    <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <SetupUILanguage>
        <UILanguage>en-US</UILanguage>
      </SetupUILanguage>
      <InputLocale>0809:00000809</InputLocale>
      <SystemLocale>en-GB</SystemLocale>
      <UILanguage>en-US</UILanguage>
      <UILanguageFallback>en-GB</UILanguageFallback>
      <UserLocale>en-GB</UserLocale>
    </component>
    <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <DiskConfiguration>
        <Disk wcm:action="add">
          <CreatePartitions>
            <CreatePartition wcm:action="add">
              <Type>Primary</Type>
              <Order>1</Order>
              <Size>350</Size>
            </CreatePartition>
            <CreatePartition wcm:action="add">
              <Order>2</Order>
              <Type>Primary</Type>
              <Extend>true</Extend>
            </CreatePartition>
          </CreatePartitions>
          <ModifyPartitions>
            <ModifyPartition wcm:action="add">
              <Active>true</Active>
              <Format>NTFS</Format>
              <Label>boot</Label>
              <Order>1</Order>
              <PartitionID>1</PartitionID>
            </ModifyPartition>
            <ModifyPartition wcm:action="add">
              <Format>NTFS</Format>
              <Label>SYSTEM</Label>
              <Letter>C</Letter>
              <Order>2</Order>
              <PartitionID>2</PartitionID>
            </ModifyPartition>
          </ModifyPartitions>
          <DiskID>0</DiskID>
          <WillWipeDisk>true</WillWipeDisk>
        </Disk>
      </DiskConfiguration>
      <ImageInstall>
        <OSImage>
          <InstallFrom>
            <MetaData wcm:action="add">
              <Key>/IMAGE/NAME </Key>
              <Value>Windows Server 2016 SERVERSTANDARD</Value>
            </MetaData>
          </InstallFrom>
          <InstallTo>
            <DiskID>0</DiskID>
            <PartitionID>2</PartitionID>
          </InstallTo>
        </OSImage>
      </ImageInstall>
      <UserData>
        <ProductKey>
          <!-- <Key>WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY</Key> -->
          <WillShowUI>OnError</WillShowUI>
        </ProductKey>
        <AcceptEula>true</AcceptEula>
        <FullName>Administrator</FullName>
        <Organization>ExampleOrg</Organization>
      </UserData>
    </component>
  </settings>
  <settings pass="generalize">
    <component name="Microsoft-Windows-Security-SPP" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <SkipRearm>1</SkipRearm>
    </component>
    <component name="Microsoft-Windows-PnpSysprep" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <PersistAllDeviceInstalls>false</PersistAllDeviceInstalls>
      <DoNotCleanUpNonPresentDevices>false</DoNotCleanUpNonPresentDevices>
    </component>
  </settings>
  <settings pass="oobeSystem">
    <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <InputLocale>e0809:00000809</InputLocale>
      <SystemLocale>en-GB</SystemLocale>
      <UILanguage>en-US</UILanguage>
      <UserLocale>en-GB</UserLocale>
    </component>
    <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <OOBE>
        <HideEULAPage>true</HideEULAPage>
        <HideLocalAccountScreen>true</HideLocalAccountScreen>
        <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
        <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
        <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
        <NetworkLocation>Work</NetworkLocation>
        <ProtectYourPC>1</ProtectYourPC>
      </OOBE>
      <RegisteredOrganization>ExampleOrg</RegisteredOrganization>
      <RegisteredOwner>ExampleUser</RegisteredOwner>
      <TimeZone>GMT Standard Time</TimeZone>
      <UserAccounts>
        <AdministratorPassword>
          <Value>example_password</Value>
          <PlainText>true</PlainText>
        </AdministratorPassword>
      </UserAccounts>
      <AutoLogon>
        <Password>
          <Value>example_password</Value>
          <PlainText>true</PlainText>
        </Password>
        <Enabled>true</Enabled>
        <Username>administrator</Username>
      </AutoLogon>
      <FirstLogonCommands>
        <SynchronousCommand wcm:action="add">
          <CommandLine>cmd.exe /c C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File a:\winrm.ps1</CommandLine>
          <Order>1</Order>
        </SynchronousCommand>
      </FirstLogonCommands>
    </component>
  </settings>
  <settings pass="specialize">
    <!-- <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-ServerManager-SvrMgrNc" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <DoNotOpenServerManagerAtLogon>true</DoNotOpenServerManagerAtLogon>
    </component> -->
    <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-IE-ESC" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <IEHardenAdmin>false</IEHardenAdmin>
      <IEHardenUser>true</IEHardenUser>
    </component>
    <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-OutOfBoxExperience" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <DoNotOpenInitialConfigurationTasksAtLogon>true</DoNotOpenInitialConfigurationTasksAtLogon>
    </component>
  </settings>
</unattend>

Just like with the unattend.xml file, you’ll want to edit this XML config to suit your needs. The product key (commented out) is the publicly-shared Windows Server 2016 KMS key. It is commented out to allow Windows to install in 180-day evaluation mode. For production usage, in a KMS environment, uncomment that line.

Running the template to start a build

Open PowerShell and navigate to the directory you have stored the scripts in (taking note of the directory structure), and run:

packer build windows_2016-base.json

This instructs Packer to download the ISOs (Windows and VirtualBox Additions), fire up VirtualBox and make the magic happen.

Packer is hugely versatile and works especially well for providing consistent code-controlled builds. The Packer JSON files are human readable and well documented allowing a low footprint of entry. Provisioned alongside a creation pipeline, using tools like Jenkins also facilitates a great deal of automation for keeping those gold master images up to date, patched and secure without the need for a lot of human intervention.

Hope you found this blog useful. As stated in the introduction, we will be incorporating this build process for our customers very soon, so look out for future blogs on this topic. Please email awilliams@ukcloud.com if you have any questions.

To view the code above in GitHub, click here.

Related Articles

https://www.packer.io/docs/templates/index.html

https://www.packer.io/docs/builders/virtualbox-iso.html

https://hodgkins.io/best-practices-with-packer-and-windows

Download Links

https://www.packer.io/downloads.html

https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2016

https://github.com/rgl/packer-provisioner-windows-update/releases

 

Our expert author

Sarah Van Der Veken

Related features

Blogs

Five reasons why we love Scotland’s vision of a digital future

The announcement of the new Cloud Services Framework from the Scottish Government brings renewed focus to their Digital Strategy. A strategy which...
Blogs

I love it when an ecosystem comes together

5 July 2019   At UKCloud, we’ve always been focussed on enabling the UK Public Sector with a vibrant partner ecosystem – it’s a deep...
Blogs

The Multi-Cloud experts behind the scenes: Using Packer to automate Gold Image creation

The concept of gold master as an image of an operating system, which is then used as a template to build operating systems for future builds, is...
Blogs

How G-Cloud helps us keep pace with our customers

I’d like to say it’s been a busy few months between the announcement of the Digital Alpha investment and our success on the latest G-Cloud...