Packer’s sysprep and generalize commands are the secret sauce for creating reusable Windows AMIs, but their interaction is far more nuanced than just running a command.
Let’s see Packer in action, building a Windows Server 2019 AMI with sysprep.
packer build \
-var 'aws_access_key=AKIAXXXXXXXXXXXXXXXX' \
-var 'aws_secret_key=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' \
-var 'region=us-east-1' \
-var 'source_ami=ami-0abcdef1234567890' \
-var 'instance_type=t3.medium' \
-var 'ssh_username=Administrator' \
-var 'ssh_password=MySecurePassword123!' \
windows_server_2019.json
Here’s the windows_server_2019.json file:
{
"builders": [
{
"type": "amazon-ebs",
"region": "{{user `region`}}",
"source_ami": "{{user `source_ami`}}",
"instance_type": "{{user `instance_type`}}",
"ssh_username": "{{user `ssh_username`}}",
"ssh_password": "{{user `ssh_password`}}",
"ami_name": "windows-server-2019-base-{{timestamp}}",
"communicator": "winrm",
"winrm_username": "{{user `ssh_username`}}",
"winrm_password": "{{user `ssh_password`}}",
"winrm_timeout": "5m",
"run_tags": {
"Name": "Packer Builder - Windows Server 2019"
},
"snapshot_tags": {
"Name": "Packer AMI - Windows Server 2019"
},
"user_data_paths": [
"scripts/join_domain.ps1"
],
"extra_arguments": [
"--block-device-mapping", "DeviceName=/dev/sda1,Ebs={VolumeSize=80,DeleteOnTermination=true}",
"--block-device-mapping", "DeviceName=/dev/xvdf,Ebs={VolumeSize=100,DeleteOnTermination=true}"
]
}
],
"provisioners": [
{
"type": "windows-shell",
"inline": [
"Write-Host 'Waiting for WinRM to be ready...'",
"while (-not (Test-Connection -ComputerName $env:COMPUTERNAME -Port 5985 -Quiet)) { Start-Sleep -Seconds 5 }"
]
},
{
"type": "windows-shell",
"inline": [
"Write-Host 'Installing Chocolatey...'",
"Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))",
"choco --version"
]
},
{
"type": "windows-shell",
"inline": [
"Write-Host 'Installing IIS and other features...'",
"Install-WindowsFeature Web-Server, Web-Asp-Net45, Telnet-Client -IncludeManagementTools",
"Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServerRole -All",
"Enable-WindowsOptionalFeature -Online -FeatureName IIS-ASPNET45 -All",
"Enable-WindowsOptionalFeature -Online -FeatureName IIS-NetFxExtensibility45 -All"
]
},
{
"type": "windows-shell",
"inline": [
"Write-Host 'Cleaning up temporary files...'",
"Remove-Item -Recurse -Force C:\\Windows\\Temp\\*",
"Remove-Item -Recurse -Force C:\\Users\\Administrator\\AppData\\Local\\Temp\\*",
"Remove-Item -Recurse -Force C:\\Windows\\Logs\\CBS\\*"
]
},
{
"type": "windows-shell",
"inline": [
"Write-Host 'Running Sysprep...'",
"C:\\Windows\\System32\\Sysprep\\sysprep.exe /generalize /oobe /shutdown /unattend:C:\\Windows\\System32\\Sysprep\\unattend.xml"
],
"expect_disconnect": true,
"timeout": "1h"
}
]
}
The sysprep.exe /generalize /oobe /shutdown command is the core of making an image reusable. sysprep.exe prepares a Windows installation to be cloned, removing unique system information like the Security Identifier (SID), computer name, and hardware-specific drivers. This ensures that when a new instance is launched from the AMI, it gets its own unique identity, preventing conflicts on a network. The /oobe flag ensures that the system boots into the Out-of-Box Experience (OOBE) on first startup, prompting for initial configuration. /shutdown powers off the machine after sysprep completes, allowing Packer to capture the image.
The unattend.xml file, which is usually placed in C:\Windows\System32\Sysprep\ and referenced by sysprep.exe, is crucial for automating the sysprep process itself. A minimal unattend.xml might look like this:
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<settings pass="generalize">
<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">
<CopyProfile>false</CopyProfile>
<ProtectUserAccounts>false</ProtectUserAccounts>
</component>
</settings>
<settings pass="oobeSystem">
<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>
<HidePasswordRequired>true</HidePasswordRequired>
<HideSmsSetting>true</HideSmsSetting>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<NetworkLocation>Work</NetworkLocation>
<ProtectUserAccounts>false</ProtectUserAccounts>
<SkipMachinePassword>true</SkipMachinePassword>
<SkipUserOOBE>true</SkipUserOOBE>
</OOBE>
<TimeZone>UTC</TimeZone>
</component>
</settings>
</unattend>
This unattend.xml tells Sysprep to skip the EULA, user account creation, and other OOBE prompts, allowing the instance to boot directly to a usable state after creation from the AMI. CopyProfile set to false is important because it prevents the Administrator profile’s settings from being copied to the default user profile, which can lead to unexpected behavior or errors when new users are created.
The expect_disconnect: true in Packer’s windows-shell provisioner for Sysprep is critical. When sysprep.exe /shutdown runs, it terminates the Windows operating system, causing the WinRM connection to drop. Packer needs to be told to expect this disconnection gracefully, otherwise, it will interpret the dropped connection as an error and fail the build.
The user_data_paths in the builder block allows you to include scripts that run before the instance is sysprepped. This is where you’d typically join the domain, install core software, or perform other setup tasks that you want to be part of the base image. However, these scripts run in the context of the current machine, not the generalized image.
A common pitfall is forgetting to install necessary drivers or software before running Sysprep. Sysprep is designed to generalize the OS, not to install new applications or drivers. If you need specific software or drivers in your AMI, they must be installed and configured on the instance before the Sysprep command is executed.
Another subtle point is the winrm_timeout. If your system is slow to respond after reboots or during provisioning steps, you might hit this timeout. For a Sysprep operation, which can take a significant amount of time, increasing this to 1h or more is often necessary.
The extra_arguments for --block-device-mapping are used to define the EBS volumes attached to the instance. Here, we’re ensuring the root volume (/dev/sda1) is 80GB and a secondary volume (/dev/xvdf) is 100GB, both set to be deleted on termination. This is part of defining the base infrastructure for your AMI.
The next error you’ll likely encounter after fixing Sysprep issues is related to WinRM connectivity after the AMI is launched.