Differences in creating Windows 10 and Windows 11 images on VMware vSphere with Packer.

Not so long a ago I wanted to switch my Windows 10 image creation based on Microsoft MDT and in-guest PowerShell scripts to HasiCorp Packer on my VMware vSphere and Horizon homelab environment. A lot of the vCommunity members wrote blogs about this image creation with Packer, so in this blog I don’t want to spend too much time how to create an image with Packer.

After reading some background information about creating images with Packer. I found out that you have two ‘scripting’ languages, one is based on “JSON” and the other one is based on HasiCorp’s own coding language called “HCL2”. What I heard is that HCL2 should be more feature proof/ready, but was really in doubt what to do, because most of the blogs were written in JSON. It is in my nature to discover new things and went for creating my images with “HCL2” and WinRM (PowerShell Remoting) scripts.

Windows 11 was announced for the 5th of October 2021 and got my fully optimized VMware Horizon – Windows 10 VDI image ready at the end August. Just hold myself from the Windows 11 Preview version, because most of the time it is not the final RTM (Release to Manufacturing) or GA (General Availability) version. My first thoughts were it should be ‘only’ switching the iso-file right???… and you can guess, I was WRONG!!! This blog will go about what I needed to change before I got everything creating fine. I will explain this step-by-step.

Moving from Windows 10 to Windows 11

Hurdle 1 (Firmware and boot issues)

The first hurdle I needed to take was switching my vSphere’s VM boot mechanism from BIOS to (U)EFI, why? BIOS is a legacy mechanism, Windows 10 supports UEFI and VMware gives the advise in 2019 already to move to UEFI boot if you can. I didn’t do this earlier, because the blog I used was based on BIOS and when changing to UEFI myself the image creation with Packer failed.
A requirements of Windows 11 is, that it only works with UEFI (and Secure Boot). Within Packer you’ll need to change configuration option “firmware” from bios to efi in the source section. But you said two sentence ago, that when you changed it to UEFI your image creation failed. Yes this time also, time for looking into a fix. Two things went wrong in this process:

  1. With UEFI set, the virtual cd-rom drive (with windows 11 iso mounted) isn’t detected as a boot device. To fix this, the configuration option “cdrom_type” needs to be set to “sata” in the source section.
    cdrom_type = "sata"
  2. Now the virtual cd-rom drive is being detected and you will see below message in the VMware Console (web or client):

    Where the firmware is set to BIOS, the ISO is being started eventually, because it is the only working boot mechanism, but UEFI doesn’t work that way.
    What now… we are trying to do some automation and manually pressing any key in this process isn’t what I, but I think you too want.
    There are two options to fix is, a hard one is editting the iso and change/rename the cdboot.efi. Within Packer we can fix it as well to implement the following configuration options into the source section.
    boot_wait    = "3s"
    boot_command = [   
    "<spacebar><spacebar>"
    ]

Hurdle 2 (Bypass Windows 11 Installation Prerequisites)

When Microsoft announced Windows 11 immediatly the kinda harsh hardware requirements made public.
Only Intel CPU’s from the 8th Gen and higher, a 64GB OS-disk and 4GB RAM memory is needed to go through the installation wizard. My VMware ESXi hosts (and virtual machines) meets this requirement, but I don’t have TPM 2.0 chip enabled. If you want to make use of the TPM functionality in VMware vSphere, then you need a Key Provider infrastructure (like HyTrust). I don’t have this kind of infrastructure in my homelab, so TPM is not an option. I think a lot of you won’t use this feature in a homelab or even in a production environment.
Windows 11 won’t allow you to install out-of-the-box unless they detect a TPM module.

Another vCommunity member (Ivo Beerens) wrote a blog how you can bypass the prerequisite checks for the Windows 11 installation.
His blog can be found here.

Yet again, we don’t want to do manual things in an automation process. Somehow I need to create this registry keys during the load of the Windows 11 installation.
In Packer we use an “autounattend.xml” which is loaded via a floppy drive to make an untouched Windows installation. The solution is to use the autounattend.xml to add the necessary registry keys. For my setup this is only bypassing the TPM check, but the other bypass option are possible as well.
Below you’ll find a part of the autounattend.xml, where I used the line of code:

        <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">
				<DiskID>0</DiskID>
				<WillWipeDisk>true</WillWipeDisk>
				<CreatePartitions>
					<CreatePartition wcm:action="add">
						<Order>1</Order>
						<Size>1000</Size>
						<Type>Primary</Type>
					</CreatePartition>
					<CreatePartition wcm:action="add">
						<Order>2</Order>
						<Size>100</Size>
						<Type>EFI</Type>
					</CreatePartition>
					<CreatePartition wcm:action="add">
						<Order>3</Order>
						<Size>128</Size>
						<Type>MSR</Type>
					</CreatePartition>
					<CreatePartition wcm:action="add">
						<Order>4</Order>
						<Extend>true</Extend>
						<Type>Primary</Type>
					</CreatePartition>
				</CreatePartitions>
				<ModifyPartitions>
					<ModifyPartition wcm:action="add">
						<Order>1</Order>
						<PartitionID>1</PartitionID>
						<Format>NTFS</Format>
						<Label>WindowsRecovery</Label>
						<TypeID>DE94BBA4-06D1-4D40-A16A-BFD50179D6AC</TypeID>
					</ModifyPartition>
					<ModifyPartition wcm:action="add">
						<Order>2</Order>
						<PartitionID>2</PartitionID>
						<Label>System</Label>
						<Format>FAT32</Format>
					</ModifyPartition>
					<ModifyPartition wcm:action="add">
						<Order>3</Order>
						<PartitionID>3</PartitionID>
					</ModifyPartition>
					<ModifyPartition wcm:action="add">
						<Order>4</Order>
						<PartitionID>4</PartitionID>
						<Letter>C</Letter>
						<Label>Windows</Label>
						<Format>NTFS</Format>
					</ModifyPartition>
				</ModifyPartitions>
			</Disk>
		</DiskConfiguration>
		<ImageInstall>
                <OSImage>
                    <InstallFrom>
                        <MetaData wcm:action="add">
                            <Key>/IMAGE/NAME</Key>
                            <Value>Windows 10 Enterprise</Value>
                        </MetaData>
                    </InstallFrom>
                    <InstallTo>
                        <DiskID>0</DiskID>
                        <PartitionID>4</PartitionID>
                    </InstallTo>
					<WillShowUI>OnError</WillShowUI>
                    <InstallToAvailablePartition>false</InstallToAvailablePartition>
                </OSImage>
        </ImageInstall>
		<RunSynchronous>
			<RunSynchronousCommand>
				<Order>1</Order>
				<!-- This sets the power scheme to high performance in WinPE for faster imaging -->
				<Path>cmd /c powercfg.exe /s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c</Path>
			</RunSynchronousCommand>
			<RunSynchronousCommand>
				<Order>2</Order>
				<!-- This sets a bypass for TPM 2.0 check -->
				<Path>cmd /c reg add &quot;HKLM\SYSTEM\Setup\LabConfig&quot; /f /v BypassTPMCheck /t REG_DWORD /d 1</Path>
			</RunSynchronousCommand>			
		</RunSynchronous>
        </component>

Different fora says that Microsoft will remove these kinds of bypass options, this was since the Windows 11 Insider Preview. In the official release this bypass still can be used.

Partition Layout
The firmware change also triggers a new disk partition layout, keep that in mind. With the old BIOS firmware, only one partition was necessary!

Version selection
Do you see another weird thing in this part of the “autounattend.xml“?…
Jup the “/IMAGE/NAME” is still “Windows 10 Enterprise“, tried to change it to “Windows 11 Enterprise“, but that doesn’t work. Is it a bad ISO I used or did Microsoft made a mistake?
Nope I don’t have a bad ISO. Used the official ISO which I download via the the MediaCreationToolW11.exe… @Microsoft, what went wrong?

Hurdle 3 (Enable Windows Feature .NET Framework 3.5)

This wasn’t really a hurdle in my switch to Windows 11, because I anticipated by copying the cabinet (.cab) file from the Windows 11 ISO to my software repository. The installation parameters with “dism.exe” stayed the same as for Windows 10.

Hurdle 4 (Start Menu Layout)

One of the last steps is in my Windows 10 image creation process, just before housekeeping, is doing some image customization like setting a default start menu layout. In Windows 10 you could make a start for the user and let them customize it. Microsoft changed something (as you can visually can see as well) in the Taskbar and Start Menu. Running the PowerShell command:
Export-StartLayout -Path "C:\Temp\LayoutModification.xml"

Doesn’t work anymore, because Microsoft changed the layout to a JSON file format (UTF-8). The PowerShell command must now be:
Export-StartLayout -Path "C:\Temp\LayoutModification.json"

In Windows 10 I copied the LayoutModification.xml to C:\Users\Default\%AppData%\Local\Microsoft\Windows\Shell all new users with no profile will get the created Start Menu Layout. Pity enough I still can’t get it to work in Windows 11. Tested the following:

  1. Export-StartLayout -UseDesktopApplicationID "C:\Temp\LayoutModification.xml", which gives the old XML format, but placing it in C:\Users\Default\%AppData%\Local\Microsoft\Windows\Shell doesn’t work for me.
  2. Followed the procedure at https://docs.microsoft.com/en-us/windows-hardware/customize/desktop/customize-the-windows-11-start-menu, manually created the JSON based on the OEMPins, doesn’t work for me.

So if somebody found a solution for this, please leave a message on my socials. A GPO I don’t want to use, because that makes the Start Menu not customizable by the user.

Hurdle 5 (Installation of Microsoft Edge Enterprise)

Microsoft Edge Enterprise is in Windows 10 by default since 20H2 version and of course Windows 11 has it as well, but the starting version will be different and not always the auto update will kick-in during the imaging process. While using the lastest downloaded version, doesn’t always mean this will also be the latest version on Windows (auto updated or not). This phenomenon needs to be checked before running the Microsoft Edge Enterprise installer. My installation script will check which version of Windows is being used and which Edge version is already installed. If my download is older, than the version on Windows during the imaging it will skip the installation process, but will disable all autoupdate functions. This is the part of my script where I do the “checks”:

Write-Host "Installing Microsoft Edge Enterprise..."
$msedge_file = (Get-Item "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe").VersionInfo
if ($msedge_file.ProductVersion -eq "$env:edge_enterprise_version" -or $msedge_file.ProductVersion -gt "$env:edge_enterprise_version" ) {
	Write-Host "This version of higher Microsoft Edge Enterprise ($env:edge_enterprise_version) is already installed, probably during autoupdate in Windows!"

	# DELETE START MENU SHORTCUT(S) IN WINDOWS
	$winversion = (Get-WmiObject Win32_OperatingSystem).Version
	if ($winversion -like "10.0.1*" ) {
		Write-Host "Deleting Microsoft Edge Enterprise desktop shortcut..."
		Remove-Item -Path "C:\Users\Public\Desktop\Microsoft Edge.lnk" -Recurse -Force | Out-Null
		#Remove-Item -Path "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk" -Recurse -Force | Out-Null
	}

	# DISABLE AUTOUPDATE FUNCTIONALITY
	Write-Host "Disable AutoUpdate Scheduled Tasks from Microsoft Edge at User Logon..."
	$taskName = "MicrosoftEdgeUpdateTaskMachineCore"
	$taskExists = Get-ScheduledTask -TaskName "$taskName" -ErrorAction SilentlyContinue
	if ($taskExists.TaskName -eq "$taskName") { 
		Get-ScheduledTask -TaskName $taskExists.TaskName | Disable-ScheduledTask | Out-Null 
		Write-Host "Scheduled Task: `"$($taskExists.TaskName)`" is set to disabled!"
	} 
	else { 
		Write-Host "Scheduled Task: `"$($taskExists.TaskName)`" doesn't exists..."
	}

	$taskName = "MicrosoftEdgeUpdateTaskMachineUA"
	$taskExists = Get-ScheduledTask -TaskName "$taskName" -ErrorAction SilentlyContinue
	if ($taskExists.TaskName -eq "$taskName") { 
		Get-ScheduledTask -TaskName $taskExists.TaskName | Disable-ScheduledTask | Out-Null 
		Write-Host "Scheduled Task: `"$($taskName)`" is set to disabled!"
	} 
	else { 
		Write-Host "Scheduled Task: `"$($taskName)`" doesn't exists..."
	}

	$taskName = "MicrosoftEdgeUpdateBrowserReplacementTask"
	$taskExists = Get-ScheduledTask -TaskName "$taskName" -ErrorAction SilentlyContinue
	if ($taskExists.TaskName -eq "$taskName") { 
		Get-ScheduledTask -TaskName $taskExists.TaskName | Disable-ScheduledTask | Out-Null 
		Write-Host "Scheduled Task: `"$($taskName)`" is set to disabled!"
	} 
	else { 
		Write-Host "Scheduled Task: `"$($taskName)`" doesn't exists..."
	}

	Write-Host "Disable Microsoft Edge update services..."
	Stop-Service -Name "edgeupdate" -Force -Confirm:$false
	Set-Service -Name "edgeupdate" -StartupType "Disabled" -Confirm:$false
	Stop-Service -Name "edgeupdatem" -Force -Confirm:$false
	Set-Service -Name "edgeupdatem" -StartupType "Disabled" -Confirm:$false
	Stop-Service -Name "MicrosoftEdgeElevationService" -Force -Confirm:$false
	Set-Service -Name "MicrosoftEdgeElevationService" -StartupType "Disabled" -Confirm:$false
	if( -Not (Test-Path -Path "HKLM:\SOFTWARE\Policies\Microsoft\EdgeUpdate" ) ) { New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\EdgeUpdate" -Force -Confirm:$false | Out-Null }
	New-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\EdgeUpdate" -Name "AutoUpdateCheckPeriodMinutes" -Type dword -Value "0" | Out-Null
	Rename-Item -Path "${env:ProgramFiles(x86)}\Microsoft\EdgeUpdate\MicrosoftEdgeUpdate.exe" "${env:ProgramFiles(x86)}\Microsoft\EdgeUpdate\NoMicrosoftEdgeUpdate.exe" -Force -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
}
else {
	$InstallExitCode = InstallMSI -MSIName "$env:edge_enterprise_dirname" -MSIArgs "/qn REBOOT=ReallySuppress"
	if (($InstallExitCode -eq 0) -or ($InstallExitCode -eq 3010)) { 
		# PLACE HERE POST INSTALL ACTIONS AFTER A SUCCESSFUL INSTALL

		# DELETE START MENU SHORTCUT(S) IN WINDOWS
		$winversion = (Get-WmiObject Win32_OperatingSystem).Version
		if ($winversion -like "10.0.1*" ) {
			Write-Host "Deleting Microsoft Edge Enterprise desktop shortcut..."
			Remove-Item -Path "C:\Users\Public\Desktop\Microsoft Edge.lnk" -Recurse -Force | Out-Null
			#Remove-Item -Path "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk" -Recurse -Force | Out-Null
		}
		
		# DISABLE AUTOUPDATE FUNCTIONALITY
		Write-Host "Disable AutoUpdate Scheduled Tasks from Microsoft Edge at User Logon..."
		$taskName = "MicrosoftEdgeUpdateTaskMachineCore"
		$taskExists = Get-ScheduledTask -TaskName "$taskName" -ErrorAction SilentlyContinue
		if ($taskExists.TaskName -eq "$taskName") { 
			Get-ScheduledTask -TaskName $taskExists.TaskName | Disable-ScheduledTask | Out-Null 
			Write-Host "Scheduled Task: `"$($taskExists.TaskName)`" is set to disabled!"
		} 
		else { 
			Write-Host "Scheduled Task: `"$($taskExists.TaskName)`" doesn't exists..."
		}

		$taskName = "MicrosoftEdgeUpdateTaskMachineUA"
		$taskExists = Get-ScheduledTask -TaskName "$taskName" -ErrorAction SilentlyContinue
		if ($taskExists.TaskName -eq "$taskName") { 
			Get-ScheduledTask -TaskName $taskExists.TaskName | Disable-ScheduledTask | Out-Null 
			Write-Host "Scheduled Task: `"$($taskName)`" is set to disabled!"
		} 
		else { 
			Write-Host "Scheduled Task: `"$($taskName)`" doesn't exists..."
		}

		$taskName = "MicrosoftEdgeUpdateBrowserReplacementTask"
		$taskExists = Get-ScheduledTask -TaskName "$taskName" -ErrorAction SilentlyContinue
		if ($taskExists.TaskName -eq "$taskName") { 
			Get-ScheduledTask -TaskName $taskExists.TaskName | Disable-ScheduledTask | Out-Null 
			Write-Host "Scheduled Task: `"$($taskName)`" is set to disabled!"
		} 
		else { 
			Write-Host "Scheduled Task: `"$($taskName)`" doesn't exists..."
		}
		
		Write-Host "Disable Microsoft Edge update services..."
		Stop-Service -Name "edgeupdate" -Force -Confirm:$false
		Set-Service -Name "edgeupdate" -StartupType "Disabled" -Confirm:$false
		Stop-Service -Name "edgeupdatem" -Force -Confirm:$false
		Set-Service -Name "edgeupdatem" -StartupType "Disabled" -Confirm:$false
		Stop-Service -Name "MicrosoftEdgeElevationService" -Force -Confirm:$false
		Set-Service -Name "MicrosoftEdgeElevationService" -StartupType "Disabled" -Confirm:$false
		if( -Not (Test-Path -Path "HKLM:\SOFTWARE\Policies\Microsoft\EdgeUpdate" ) ) { New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\EdgeUpdate" -Force -Confirm:$false | Out-Null }
		New-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\EdgeUpdate" -Name "AutoUpdateCheckPeriodMinutes" -Type dword -Value "0" | Out-Null
		Rename-Item -Path "${env:ProgramFiles(x86)}\Microsoft\EdgeUpdate\MicrosoftEdgeUpdate.exe" "${env:ProgramFiles(x86)}\Microsoft\EdgeUpdate\NoMicrosoftEdgeUpdate.exe" -Force -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
	}
	else {
		# PLACE HERE POST INSTALL ACTIONS AFTER A FAILED INSTALL
	}
}

During the first install of Microsoft Edge Enterprise it failed with a 1603 and exited the script, also disabling the autoupdate functions etc.

Hurdle 6 (UWP Apps)

The next script is removing all the (in my opinion) unnecessary “UWP Apps” (Bloatware) from the Windows 11 image… For Windows 10 I made an exclusion list from apps I still wanted or which are important to image. Using exactly that same script what I used for Windows 10 failed my imaging process, because there are two UWP Apps in Windows 11 which are not allowed to be removed.
Those two UWP Apps are “Microsoft.DesktopAppInstaller” and “Microsoft.SecHealthUI”, exluding these in my Remove-AppxProvisionedPackage fixed the errors where my imaging stopped.
My code to remove the unnecessary UWP Apps is:

Write-Host "Remove All Unwanted Windows Built-in Store Apps for All New Users in UI..."
Get-AppxPackage -AllUsers | Where-Object {$_.IsFramework -Match 'False' -and $_.NonRemovable -Match 'False' -and $_.Name -NotMatch 'Microsoft.StorePurchaseApp' -and $_.Name -NotMatch 'Microsoft.WindowsStore' -and $_.Name -NotMatch 'Microsoft.MSPaint' -and $_.Name -NotMatch 'Microsoft.Windows.Photos' -and $_.Name -NotMatch 'Microsoft.WindowsCalculator'} | Remove-AppxPackage -ErrorAction SilentlyContinue

Write-Host "Remove All Unwanted Windows Built-in Store Apps for the Current User in UI..."
Get-AppxPackage | Where-Object {$_.IsFramework -Match 'False' -and $_.NonRemovable -Match 'False' -and $_.Name -NotMatch 'Microsoft.StorePurchaseApp' -and $_.Name -NotMatch 'Microsoft.WindowsStore' -and $_.Name -NotMatch 'Microsoft.MSPaint' -and $_.Name -NotMatch 'Microsoft.Windows.Photos' -and $_.Name -NotMatch 'Microsoft.WindowsCalculator'} | Remove-AppxPackage -ErrorAction SilentlyContinue

Write-Host "Remove All Unwanted Windows Built-in Store Apps files from Disk..."
$UWPapps = Get-AppxProvisionedPackage -Online | Where-Object {$_.PackageName -NotMatch 'Microsoft.StorePurchaseApp' -and $_.PackageName -NotMatch 'Microsoft.WindowsStore' -and $_.PackageName -NotMatch 'Microsoft.MSPaint' -and $_.PackageName -NotMatch 'Microsoft.Windows.Photos' -and $_.PackageName -NotMatch 'Microsoft.WindowsCalculator' -and $_.PackageName -NotMatch 'Microsoft.DesktopAppInstaller' -and $_.PackageName -NotMatch 'Microsoft.SecHealthUI'}
Foreach ($UWPapp in $UWPapps) {
    Remove-ProvisionedAppxPackage -PackageName $UWPapp.PackageName -Online -ErrorAction SilentlyContinue
}

Hurdle 7 (VMware OS Optimization Tool)

The ultimum VDI/RDS optimization tool for VMware based VDI/RDS images is the VMware Fling called “VMware OSOT (OS Optimization Tool)“. If you don’t know about it or make your own optimizations take a look at it. The (v)Community is creating and bundeling the best templates together and can be analyzed and deployment in a couple of click. At the moment 2107 is the latest version while writing. It doesn’t have a (new) template for Windows 11, so the best optimizations for this operating system still needs te done. From what I have heard during the “Login VSI Summit” from one of the creators is that they expect/hope to have template somewhere in Q1 of 2022. Although Windows 11 is supported since the launch of it on VMware vSphere/Horizon (see the kb-article here), they are stil gathering issues found by early adopters. Until then, we need to wait.zResult

Result

The VDI image build process with Packer took about 1 hour and 10 minutes, this is including all my master image software, Microsoft Updates and housekeeping of the image.
Currently Windows 11 is running pretty well as a non-persistent VDI image on VMware Horizon environment in my homelab.
For now I don’t have any issues found so far…

AutoLogon to Windows 11 during Packer imaging process.


If you have any further questions about one of my topics, please reach me on LinkedIn or Twitter.