Packer Overview and Why Use It for Windows Server Images

Packer is an open-source tool from HashiCorp that automates the creation of identical machine images for multiple platforms from a single configuration source. Instead of manually installing and configuring Windows Server 2022, patching it, installing agents, and then capturing it — a process prone to inconsistency — Packer performs all of these steps programmatically and produces a reproducible, versioned image artifact.

The primary benefits for Windows Server 2022 image builds are consistency across environments (Hyper-V, VMware, Azure, AWS all get the same baseline), version-controlled build configuration, automated Windows Update integration, and the ability to layer software provisioning on top of a fully patched base OS before the image is captured.

Packer uses a concept of builders (which platform to create the VM on), provisioners (scripts that run inside the VM to configure it), and post-processors (what to do with the finished image). All of this is defined in HCL2 (HashiCorp Configuration Language) template files.

Installing Packer on Windows Server 2022

Packer is distributed as a single binary. The simplest installation method on Windows Server 2022 is via Chocolatey or by downloading the binary directly from HashiCorp.

# Method 1: Install via Chocolatey (if Chocolatey is already installed)
choco install packer -y

# Method 2: Manual installation
# Download the latest Packer binary
$url = "https://releases.hashicorp.com/packer/1.10.0/packer_1.10.0_windows_amd64.zip"
Invoke-WebRequest -Uri $url -OutFile "C:Temppacker.zip"
Expand-Archive -Path "C:Temppacker.zip" -DestinationPath "C:ToolsPacker"

# Add to system PATH
$currentPath = [System.Environment]::GetEnvironmentVariable("PATH", "Machine")
[System.Environment]::SetEnvironmentVariable("PATH", "$currentPath;C:ToolsPacker", "Machine")
$env:PATH += ";C:ToolsPacker"

# Verify installation
packer version
# Output: Packer v1.10.0

# Initialize plugins for the build (required for HCL2 templates)
# Run from your template directory:
packer init .

Packer HCL2 Template Structure

Modern Packer (v1.7+) uses HCL2 as the primary template format. A Packer project for Windows Server 2022 typically consists of several files in the same directory: the main template file (windows-2022.pkr.hcl), a variables file (variables.pkrvars.hcl), and supporting files like autounattend.xml.

The HCL2 template starts with a packer block declaring required plugins, followed by source blocks defining the builder configuration, and a build block tying everything together:

# windows-2022.pkr.hcl

packer {
  required_plugins {
    hyperv = {
      version = ">= 1.1.3"
      source  = "github.com/hashicorp/hyperv"
    }
    vmware = {
      version = ">= 1.0.11"
      source  = "github.com/hashicorp/vmware"
    }
    azure = {
      version = ">= 2.1.3"
      source  = "github.com/hashicorp/azure"
    }
  }
}

variable "iso_url" {
  type    = string
  default = "https://software-static.download.prss.microsoft.com/sg/download/888969d5-f34g-4e03-ac9d-1f9786c66749/SERVER_EVAL_x64FRE_en-us.iso"
}

variable "iso_checksum" {
  type    = string
  default = "sha256:3e4fa6d8507b554856fc9ca6079cc402df11a8b79344871669f0251535255325"
}

variable "winrm_username" {
  type    = string
  default = "Administrator"
}

variable "winrm_password" {
  type      = string
  default   = "Packer@Build2022!"
  sensitive = true
}

Configuring the WinRM Communicator

Unlike Linux Packer builds that use SSH, Windows Server 2022 builds use WinRM (Windows Remote Management) as the communicator. Packer connects to the VM via WinRM to run provisioner scripts after the OS installs. WinRM must be enabled and configured during the unattended install via autounattend.xml.

# In the source block, configure WinRM:
source "hyperv-iso" "windows2022" {
  iso_url      = var.iso_url
  iso_checksum = var.iso_checksum

  # VM settings
  vm_name           = "WS2022-Base"
  cpus              = 4
  memory            = 4096
  disk_size         = 61440   # 60 GB in MB
  generation        = 2
  enable_secure_boot = false  # Disable for unattended install compatibility
  switch_name       = "Default Switch"

  # Boot command to trigger autounattend
  boot_command = [""]
  boot_wait    = "5s"

  # WinRM communicator settings
  communicator   = "winrm"
  winrm_username = var.winrm_username
  winrm_password = var.winrm_password
  winrm_use_ssl  = false
  winrm_insecure = true
  winrm_timeout  = "4h"  # Long timeout to account for Windows Update

  # Point to autounattend.xml via virtual floppy
  floppy_files = [
    "autounattend.xml",
    "scripts/enable-winrm.ps1",
    "scripts/set-temp-password.ps1"
  ]

  shutdown_command = "shutdown /s /t 10 /f /d p:4:1 /c "Packer Shutdown""
  shutdown_timeout = "30m"
}

Packer autounattend.xml for Unattended Windows Server 2022 Install

The autounattend.xml file is the Windows Answer File that automates the Windows installation. It is placed on a virtual floppy disk that Packer mounts to the VM. This file handles disk partitioning, product key entry, locale settings, administrator password configuration, and — critically — enabling WinRM so Packer can connect.



  
    
      
        en-US
      
      en-US
      en-US
      en-US
      en-US
    
    
      
        
          
            
              1
              EFI
              500
            
            
              2
              MSR
              128
            
            
              3
              Primary
              true
            
          
          
            
              1
              1
              
              FAT32
            
            
              2
              3
              
              NTFS
              C
            
          
          0
          true
        
      
      
        
          
            0
            3
          
          false
        
      
      
        
          
          WX4NM-KYWYW-QJJR4-XV3QB-6VM33
          Never
        
        true
        Packer
        MyOrg
      
    
  
  
    
      PACKER-BUILD
    
  
  
    
      
        
          Packer@Build2022!
          true
        
        true
        Administrator
        5
      
      
        
          1
          cmd /c "a:enable-winrm.ps1"
          Enable WinRM for Packer
          false
        
      
      
        true
        true
        true
        true
        true
        1
        true
        true
      
      
        
          Packer@Build2022!
          true
        
      
    
  

The enable-winrm.ps1 script referenced in FirstLogonCommands must configure WinRM to accept Packer connections:

# scripts/enable-winrm.ps1
Write-Output "Configuring WinRM for Packer..."

# Enable WinRM
Enable-PSRemoting -Force -SkipNetworkProfileCheck

# Allow unencrypted (for local network builds; use HTTPS for production)
Set-Item WSMan:localhostServiceAllowUnencrypted -Value True
Set-Item WSMan:localhostServiceAuthBasic -Value True

# Configure WinRM listener
winrm quickconfig -q
winrm set winrm/config '@{MaxTimeoutms="7200000"}'
winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="2048"}'
winrm set winrm/config/service '@{AllowUnencrypted="true"}'
winrm set winrm/config/service/auth '@{Basic="true"}'

# Open firewall for WinRM
netsh advfirewall firewall add rule name="WinRM (HTTP-In)" `
    dir=in action=allow protocol=TCP localport=5985

Write-Output "WinRM configured successfully."

Provisioners: PowerShell and Windows Shell

After Packer connects via WinRM, provisioners run sequentially to configure the image. The build block in your HCL2 template defines the provisioners:

build {
  name    = "windows-2022-base"
  sources = ["source.hyperv-iso.windows2022"]

  # Run an inline PowerShell script
  provisioner "powershell" {
    inline = [
      "Set-ExecutionPolicy Bypass -Scope Process -Force",
      "Install-WindowsFeature -Name NET-Framework-45-Core",
      "Set-TimeZone -Id 'UTC'"
    ]
  }

  # Run a local PowerShell script file
  provisioner "powershell" {
    script = "scripts/install-updates.ps1"
    execution_policy = "bypass"
    elevated_user     = "Administrator"
    elevated_password = var.winrm_password
  }

  # Windows shell (cmd.exe) provisioner
  provisioner "windows-shell" {
    inline = [
      "sc config wuauserv start= auto",
      "net start wuauserv"
    ]
  }

  # Install Chocolatey and common tools
  provisioner "powershell" {
    script = "scripts/install-tools.ps1"
  }

  # Run sysprep last, immediately before capture
  provisioner "powershell" {
    script = "scripts/sysprep.ps1"
  }
}

Installing Windows Updates in Packer

One of the most time-consuming but important parts of a gold image build is applying all available Windows updates. The install-updates.ps1 script uses the PSWindowsUpdate module to install patches non-interactively:

# scripts/install-updates.ps1
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

# Install NuGet provider and PSWindowsUpdate module
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
Install-Module -Name PSWindowsUpdate -Force -Confirm:$false

# Import and run update installation
Import-Module PSWindowsUpdate
Write-Output "Installing Windows Updates..."
Install-WindowsUpdate -AcceptAll -AutoReboot:$false -Verbose

# Log installed updates
$installedUpdates = Get-WUHistory | Select-Object -First 20
$installedUpdates | Format-Table Title, Date, Result

Write-Output "Updates complete. Rebooting..."
Restart-Computer -Force

Because Windows Update may require multiple reboots, the Packer provisioner can use the restart_check_command and restart_command options to handle reboots gracefully:

provisioner "windows-restart" {
  restart_check_command = "powershell -command "& {if ((Get-Content 'C:\packer_restart_needed.txt' -ErrorAction SilentlyContinue) -eq 'reboot_needed') { exit 1 } else { exit 0 }}""
  restart_timeout       = "30m"
}

Sysprep in Packer

Before Packer captures the image, sysprep must run to generalize it — removing machine-specific information like SIDs, computer names, and driver settings. The sysprep script runs as the final provisioner:

# scripts/sysprep.ps1
Write-Output "Starting Sysprep generalization..."

# Clean up Packer artifacts before sysprepping
Remove-Item -Path "C:WindowsTemp*" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path "C:Temp*" -Recurse -Force -ErrorAction SilentlyContinue

# Run Sysprep in OOBE generalize mode
& "C:WindowsSystem32Sysprepsysprep.exe" /oobe /generalize /quiet /quit

# Wait for sysprep to complete
do {
    Start-Sleep -Seconds 5
    $sysprepStatus = (Get-Item "HKLM:SOFTWAREMicrosoftWindows NTCurrentVersionSoftwareProtectionPlatform").GetValue("")
} while ((Get-Process -Name sysprep -ErrorAction SilentlyContinue))

Write-Output "Sysprep complete. Image is ready for capture."

Building for Hyper-V, VMware, and Azure

The same provisioner configuration can be reused across multiple builders by adding multiple source blocks. For VMware vSphere or ESXi:

source "vmware-iso" "windows2022" {
  iso_url      = var.iso_url
  iso_checksum = var.iso_checksum
  vm_name      = "WS2022-Template"
  cpus         = 4
  memory       = 4096
  disk_size    = 61440
  disk_type_id = 1  # thin provision
  guest_os_type = "windows2019srvnext-64"
  vmx_data = {
    "virtualHW.version" = "19"
    "scsi0.virtualDev"  = "pvscsi"
    "ethernet0.virtualDev" = "vmxnet3"
  }
  communicator   = "winrm"
  winrm_username = var.winrm_username
  winrm_password = var.winrm_password
  winrm_timeout  = "4h"
  floppy_files   = ["autounattend.xml", "scripts/enable-winrm.ps1"]
  shutdown_command = "shutdown /s /t 10 /f"
}

For Azure using the azure-arm builder, you build from an existing Azure Marketplace image rather than an ISO, so there is no autounattend.xml needed:

source "azure-arm" "windows2022" {
  client_id       = var.azure_client_id
  client_secret   = var.azure_client_secret
  tenant_id       = var.azure_tenant_id
  subscription_id = var.azure_subscription_id

  managed_image_resource_group_name = "rg-packer-images"
  managed_image_name                = "WS2022-Custom-{{timestamp}}"
  managed_image_storage_account_type = "Standard_LRS"

  os_type          = "Windows"
  image_publisher  = "MicrosoftWindowsServer"
  image_offer      = "WindowsServer"
  image_sku        = "2022-datacenter-azure-edition"
  image_version    = "latest"
  location         = "East US"
  vm_size          = "Standard_D4s_v3"

  communicator   = "winrm"
  winrm_use_ssl  = true
  winrm_insecure = true
  winrm_timeout  = "10m"
  winrm_username = "packer"

  # Azure handles WinRM bootstrap automatically via Azure VM Extension
  os_disk_size_gb = 128

  azure_tags = {
    Environment = "Golden-Image"
    CreatedBy   = "Packer"
  }
}

Post-Processors: Vagrant Box, Manifest, and Compress

Post-processors transform the built artifact after capture. Common post-processors for Windows Server 2022 images include vagrant (creates a .box file for Vagrant), manifest (records build metadata to a JSON file), and compress (compresses the output):

build {
  sources = ["source.hyperv-iso.windows2022"]

  # ... provisioners ...

  post-processor "vagrant" {
    output = "builds/WS2022-{{.Provider}}-{{timestamp}}.box"
    keep_input_artifact = true
  }

  post-processor "manifest" {
    output     = "builds/manifest.json"
    strip_path = true
    custom_data = {
      build_version = "1.0.0"
      build_date    = timestamp()
      os_version    = "Windows Server 2022"
    }
  }
}

To run the build for a specific builder:

# Initialize required plugins
packer init windows-2022.pkr.hcl

# Validate the template
packer validate windows-2022.pkr.hcl

# Build only the Hyper-V image
packer build -only="hyperv-iso.windows2022" windows-2022.pkr.hcl

# Build all defined images
packer build windows-2022.pkr.hcl

# Build with variable overrides
packer build -var "winrm_password=MySecurePass!" windows-2022.pkr.hcl

# Enable debug output
PACKER_LOG=1 packer build windows-2022.pkr.hcl

Packer transforms Windows Server 2022 image creation from a manual, error-prone process into a version-controlled, automated pipeline. Once the initial template and scripts are established, rebuilding with updated patches or software is a single command that produces identical, production-ready images across every target platform.