Automated Windows 10 Installation with AutoUnattend and Packer

0

Not too long ago, I covered in a post called Create Unattend Answer File for Windows Server 2019 Automated Packer Installation. In carrying this further with continued automation of Windows machines for lab environments, I wanted to put together a quick post of how to achieve an automated Windows 10 installation with AutoUnattend and Packer using both the answer file functionality provided by the autounattend and then using Packer to automate the provisioning of the VMware virtual machine. Let’s take a look at this process to quickly build out a Windows 10 VM, install updates, shut it down and transition it to a VMware template for quick cloning of an up-to-date template file to produce a ready to run Windows 10 virtual machine.

Windows 10 AutoUnattend Answer File for Use with Packer

After playing around with a few settings (much of which was stolen from my Windows Server 2019 autounattend file), I came up with the following autounattend answer file for Windows 10 Pro for the home lab.

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="windowsPE">
        <component name="Microsoft-Windows-International-Core-WinPE" 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">
            <SetupUILanguage>
                <UILanguage>en-US</UILanguage>
            </SetupUILanguage>
            <InputLocale>en-US</InputLocale>
            <SystemLocale>en-US</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UserLocale>en-US</UserLocale>
        </component>
        <component name="Microsoft-Windows-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">
            <DiskConfiguration>
                <Disk wcm:action="add">
                    <CreatePartitions>
                        <CreatePartition wcm:action="add">
                            <Order>1</Order>
                            <Size>500</Size>
                            <Type>Primary</Type>
                        </CreatePartition>
                        <CreatePartition wcm:action="add">
                            <Order>2</Order>
                            <Extend>true</Extend>
                            <Type>Primary</Type>
                        </CreatePartition>
                    </CreatePartitions>
                    <ModifyPartitions>
                        <ModifyPartition wcm:action="add">
                            <Order>1</Order>
                            <PartitionID>1</PartitionID>
                            <Format>NTFS</Format>
                            <Label>Boot</Label>
                            <Active>true</Active>
                        </ModifyPartition>
                        <ModifyPartition wcm:action="add">
                            <Order>2</Order>
                            <PartitionID>2</PartitionID>
                            <Format>NTFS</Format>
                            <Label>System</Label>
                        </ModifyPartition>
                    </ModifyPartitions>
                    <DiskID>0</DiskID>
                    <WillWipeDisk>true</WillWipeDisk>
                </Disk>
            </DiskConfiguration>
            <ImageInstall>
                <OSImage>
                    <InstallFrom>
                        <MetaData wcm:action="add">
                            <Key>/IMAGE/NAME</Key>
                            <Value>Windows 10 Pro</Value>
                        </MetaData>
                    </InstallFrom>
                    <InstallTo>
                        <DiskID>0</DiskID>
                        <PartitionID>2</PartitionID>
                    </InstallTo>
                    <WillShowUI>OnError</WillShowUI>
                    <InstallToAvailablePartition>false</InstallToAvailablePartition>
                </OSImage>
            </ImageInstall>
            <UserData>
                <AcceptEula>true</AcceptEula>
				<ProductKey>
                  <Key>11111-22222-33333-44444-55555</Key>
                  <WillShowUI>Never</WillShowUI>
               </ProductKey>
            </UserData>
        </component>
    </settings>
    <settings pass="specialize">
        <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">
            <TimeZone>Central Standard Time</TimeZone>
        </component>
        <component name="Microsoft-Windows-Deployment" 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">
            <RunSynchronous>
                <RunSynchronousCommand wcm:action="add">
                    <Path>a:\vmtools.cmd</Path>
                    <Order>1</Order>
                    <WillReboot>Always</WillReboot>
                </RunSynchronousCommand>
            </RunSynchronous>
        </component>
		 <component name="Microsoft-Windows-TerminalServices-LocalSessionManager" 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">
            <fDenyTSConnections>false</fDenyTSConnections>
        </component>
        <component name="Networking-MPSSVC-Svc" 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">
            <FirewallGroups>
                <FirewallGroup wcm:action="add" wcm:keyValue="RemoteDesktop">
                    <Active>true</Active>
                    <Group>Remote Desktop</Group>
                    <Profile>all</Profile>
                </FirewallGroup>
            </FirewallGroups>
        </component>
        <component name="Microsoft-Windows-TerminalServices-RDP-WinStationExtensions" 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">
            <SecurityLayer>2</SecurityLayer>
            <UserAuthentication>1</UserAuthentication>
        </component>
		<component name="Microsoft-Windows-ServerManager-SvrMgrNc" 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">
            <DoNotOpenServerManagerAtLogon>true</DoNotOpenServerManagerAtLogon>
        </component>
    </settings>
    <settings pass="oobeSystem">
	    <component language="neutral" name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <InputLocale>0409:00000409</InputLocale>
            <SystemLocale>en-US</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UILanguageFallback>en-US</UILanguageFallback>
            <UserLocale>en-US</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">
            <AutoLogon>
                <Password>
                    <Value>password</Value>
                    <PlainText>true</PlainText>
                </Password>
                <LogonCount>3</LogonCount>
                <Username>Administrator</Username>
                <Enabled>true</Enabled>
            </AutoLogon>            
            <UserAccounts>
                <AdministratorPassword>
                    <Value>password</Value>
                    <PlainText>true</PlainText>
                </AdministratorPassword>
            </UserAccounts>
			<OOBE>
                <HideEULAPage>true</HideEULAPage>
                <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
                <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
                <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
                <NetworkLocation>Work</NetworkLocation>
                <ProtectYourPC>1</ProtectYourPC>
                <SkipUserOOBE>true</SkipUserOOBE>
                <SkipMachineOOBE>true</SkipMachineOOBE>
            </OOBE>
			<FirstLogonCommands>
                <SynchronousCommand wcm:action="add">
                    <Order>1</Order>
                    <!-- Enable WinRM service -->
                    <CommandLine>powershell -ExecutionPolicy Bypass -File a:\configure.ps1</CommandLine>
                    <RequiresUserInput>true</RequiresUserInput>
                </SynchronousCommand>
			</FirstLogonCommands>
        </component>
    </settings>
    <cpi:offlineImage cpi:source="wim:c:/users/administrator/desktop/win10/install.wim#Windos 10 Pro" xmlns:cpi="urn:schemas-microsoft-com:cpi" />
</unattend>

Using the above Autounattend, I am able to get the machine to build, no OOBE and also run a PowerShell script to perform other tasks that I will show below.

Windows 10 PowerShell Script to Run Tasks at First Logon

The above autounattend file runs a script called configure.ps1. This script contains the following tasks:

  • Sets the network connection profile
  • Copies over a specific version of BGINFO
  • Copies over additional files as needed
  • Installs NuGet and PSWindowsUpdate module
  • Sets the WinRM configuration
  • Resets AutoLogon count
$ErrorActionPreference = "Stop"

# Switch network connection to Domain mode
# Required for WinRM firewall rules
$profile = Get-NetConnectionProfile
Set-NetConnectionProfile -Name $profile.Name -NetworkCategory Domain

# Copy BGINFO and custom INI file
net use x: \\utility01\packerbuild /persistent:no
xcopy /E X:\BGinfo "c:\program files (x86)\bginfo\"
xcopy X:\startup\bginfo.bat "C:\programdata\microsoft\windows\start menu\programs\startup"
"C:\programdata\microsoft\windows\start menu\programs\startup\bginfo.bat"

#Copy over Additional Setup Configuration
xcopy /E X:\setup "c:\windows\tools\setup\"

#Install PS Windows Update Module
Get-PackageProvider -name nuget -force
Install-Module PSWindowsUpdate -confirm:$false -force
Get-WindowsUpdate -Install -acceptall -IgnoreReboot

#WinRM Configure
winrm quickconfig -quiet
winrm set winrm/config/service '@{AllowUnencrypted="true"}'
winrm set winrm/config/service/auth '@{Basic="true"}'

# Reset auto logon count
# https://docs.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/microsoft-windows-shell-setup-autologon-logoncount#logoncount-known-issue
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name AutoLogonCount -Value 0

Windows 10 Packer JSON Files

For the variables JSON file used by Packer, this file is used to feed in the variables for the vSphere provider used during the Packer build process interacting with VMware vSphere. Of course, replace the variables below with the values that are appropriate in your VMware vSphere environment. My file is named varswin10.json.

{
    "vsphere_server": "vcsa.cloud.local",
    "vsphere_user": "[email protected]",
    "vsphere_password": "password",
    "vsphere_template_name": "Win10_1809clone",
    "vsphere_folder": "Templates",
    "vsphere_dc_name": "CloudLocal",
    "vsphere_compute_cluster": "vsancluster",
    "vsphere_host": "10.1.149.14",
    "vsphere_portgroup_name": "DPG-Servers",
    "vsphere_datastore": "vsanDatastore",
    "winadmin_password": "password",
    "cpu_num": "4",
    "mem_size": "4096",
    "disk_size": "102400",
    "os_iso_path": "[vsanDatastore] ISO/en_windows_10_business_edition_version_1809_updated_april_2019_x64_dvd_62b47844.iso",
    "vmtools_iso_path":"[vsanDatastore] ISO/windows.iso"
}

The Packert JSON file, which in my case named windows10.json that consumes the variables and actually carries out the build is as follows:

{
  "variables": {
    "vsphere_server": "",
    "vsphere_user": "",
    "vsphere_password": "",
    "vsphere_template_name": "",
    "vsphere_folder": "",
    "vsphere_dc_name": "",
    "vsphere_compute_cluster": "",
    "vsphere_host": "",
    "vsphere_portgroup_name": "",
    "vsphere_datastore": "",
    "winadmin_password": "",
    "cpu_num": "",
    "mem_size": "",
    "disk_size": "",
    "os_iso_path": "",
    "vmtools_iso_path":""
  },
  "sensitive-variables": ["vsphere_password", "winadmin_password"],
  "builders": [
    {
      "type": "vsphere-iso",

      "vcenter_server":      "{{user `vsphere_server`}}",
      "username":            "{{user `vsphere_user`}}",
      "password":            "{{user `vsphere_password`}}",
      "insecure_connection": "true",

      "vm_name": "{{user `vsphere_template_name`}}",
      "folder" : "{{user `vsphere_folder`}}",
      "datacenter": "{{user `vsphere_dc_name`}}",
      "cluster":     "{{user `vsphere_compute_cluster`}}",
      "host": "{{user `vsphere_host`}}",
      "network": "{{user `vsphere_portgroup_name`}}",
      "datastore": "{{user `vsphere_datastore`}}",
      "convert_to_template": "true",

      "guest_os_type": "windows9_64Guest",

      "communicator": "winrm",
      "winrm_username": "Administrator",
      "winrm_password": "{{user `winadmin_password`}}",

      "CPUs":             "{{user `cpu_num`}}",
      "RAM":              "{{user `mem_size`}}",
      "RAM_reserve_all": true,
      "firmware": "bios",

      "disk_controller_type":  "lsilogic-sas",
      "disk_size":        "{{user `disk_size`}}",
      "disk_thin_provisioned": true,

      "network_card": "vmxnet3",

      "iso_paths": [
        "{{user `os_iso_path`}}",
        "{{user `vmtools_iso_path`}}"
      ],
      "floppy_files": [
        "setup/win10/autounattend.xml",
        "setup/setup.ps1",
        "setup/vmtools.cmd"
      ]
    }
  ],

  "provisioners": [
    {
      "type": "windows-shell",
      "inline": ["dir c:\\"]
    }
  ]
}

Automated Windows 10 Installation with AutoUnattend and Packer

To pull it all together and use the files that are detailed above to achieve an automated Windows 10 installation with AutoUnattend and Packer, simply run the following command with the appropriate files created:

packer build -var-file varswin10.json windows 10.json
packer build -force -var-file varswin10.json windows 10.json (use force if the template already exists and you want to overwrite it)

The great thing as mentioned in the Windows Server 2019 Packer post, you can schedule the packer build to automatically rebuild the template on a schedule so that you always have the most up to date copy of Windows Server or Windows 10 variants for use in deploying in lab or production environments.

Wrapping Up

The value and power of being able to have an automated Windows 10 installation with AutoUnattend and Packer is extremely handy. Automation is the key to maximum efficiency, especially when you are deploying and tearing down tens or hundreds of servers every day/week. Thes types of processes go a long way in getting on the road to immutable infrastructure.