I recently transitioned from VMware ESXi and built a new server for a fresh Proxmox setup. I had no prior experience with Proxmox, but how complicated could a simple Windows installation be? Since installing Windows is usually boring and timeconsuming, I decided to make it more interesting by deploying it autonomously with a one-click setup. What could go wrong …
Prerequisites for Deploying Windows on Proxmox
- Proxmox Server
- Machine running Ansible
- Ansible Playbook
- Download Windows 11 Image
- Download latest Virtio Drivers
- Generate and adjust our
autounattend.xmlfiles for Windows 10/11
Ansible Design Principles
Ansible is a radically simple IT automation system. It handles configuration management, application deployment, cloud provisioning, ad-hoc task execution, network automation, and multi-node orchestration. Ansible makes complex changes like zero-downtime rolling updates with load balancers easy. More information on the Ansible website.
- Have an extremely simple setup process with a minimal learning curve.
- Manage machines quickly and in parallel.
- Avoid custom-agents and additional open ports, be agentless by leveraging the existing SSH daemon.
- Describe infrastructure in a language that is both machine and human friendly.
- Focus on security and easy auditability/review/rewriting of content.
- Manage new remote machines instantly, without bootstrapping any software.
- Allow module development in any dynamic language, not just Python.
- Be usable as non-root.
- Be the easiest IT automation system to use, ever.
Installing Ansible on a Rocky 9 VM
First off, i’ve created a Rocky 9 VM which i can use for Ansible. The installation is fairly straight forward
[root@Rocky2 ~]# dnf install python3
[root@Rocky2 ~]# dnf install python3-pip
[root@Rocky2 ~]# dnf install sshpass
[root@Rocky2 ~]# python3 -m venv /home/ansible-win11-proxmox
[root@Rocky2 ~]# source /home/ansible-win11-proxmox/bin/activate
(ansible-win11-proxmox) [root@Rocky2 ~]# /home/ansible-win11-proxmox/bin/python3 -m pip install --upgrade pip
(ansible-win11-proxmox) [root@Rocky2 ~]# pip install ansible<br>Collecting ansible
Downloading ansible-8.7.0-py3-none-any.whl (48.4 MB)Create an Ansible Playbook
Configure a Ansible playbook to create Windows templates for Proxmox (PVE) based lab environment (somewhat customized for minimal, but easy enough to tweak), prepped for further Ansible automation. Tested with Proxmox v9.0.11 & Ansible 2.15.13
You can download a template of the playbook from my github project >click here<
What each file does (and why you need them)
Inventory file (e.g. inventory.yml)
The inventory defines which machines (hosts) Ansible manages — either individually or grouped.
---
proxmox:
hosts:
10.10.2.77 #Proxmox IP Address
vars:
ansible_user: root-proxmox # Proxmox User
ansible_password: 'P@ssw0rd' # Proxmox user password
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
ansible_python_interpreter: auto_silentVariables file / vars (e.g. vars.yml, or via group_vars/, host_vars/)
Many tasks in your playbook need parameters or configuration values (e.g. port numbers, credentials, host-specific paths, etc.) Variables lets you avoid hard-coding them in the playbook.
---
# PVE Template Provisioning Variables
template_name: Win11 # Proxmox Template Name
pve_storage_id: local # Proxmox Storage location, Default: local
vm_cores: 2
vm_sockets: 2
vm_memory_mb: 4096
vm_os_type: win11
# vm_os_type must be valid value per https://pve.proxmox.com/wiki/Qemu/KVM_Virtual_Machines
drive_size_gb: 60
start_at_boot: no
os_iso_location: local:iso/Win11_25H2_English_x64.iso
format: qcow2
# qcow2 or raw if on lvm
# EFI disk (for booting via UEFI)
efidisk0:
storage: "local-lvm" # Proxmox storage for EFI disk
size: "4M" # or "2M" for minimal, "4M" if you need secure boot keys
format: "raw"
efitype: "4m"
pre_enrolled_keys: false # if you plan to use secure boot
# TPM for Windows 11 requirement (optional but recommended)
tpmstate0:
storage: "local-lvm"
size: "4M" # TPM state size, adjust as needed
version: "v2.0"
format: "qcow"
# Boot settings
boot: "order=ide0;ide1;virtio0;ide2"
cpu_type: "x86-64-v2"
#ootdisk: "virtio0"
# Use UEFI BIOS
bios: "ovmf"
# Specify machine type
machine: "pc-q35-10.1" # Example Q35 machine; adjust to your Proxmox/QEMU version
# Unattend.xml template variables:
deploy_image: 'Windows Pro 11 N'
# Applicable 2025 options for deploy_image:
#Windows 11 Home For typical consumers / home users
#Windows 11 Pro For more advanced users, small businesses — adds networking, security, business features.
#Windows 11 Pro for Workstations For high-end workstations / power-users needing extra performance / hardware support.
#Windows 11 Education For schools / educational institutions / students.
#Windows 11 Pro Education Education-oriented but with Pro-level features (for institutions needing management/security + education licensing).
#Windows 11 Enterprise For large organizations / companies needing advanced management, security, and deployment tools.
#Windows 11 SE A lightweight version — aimed at low-cost / low-end devices, especially for education.
#Windows 11 IoT Enterprise For embedded devices / specialized “Internet of Things” (IoT) scenarios / industrial use.
# The “N” editions (also “KN” for certain markets) are specially made versions that do not include media-related technologies.
# In practice this means: no built-in media playback apps (no Windows Media Player, no built-in video/music apps, no preinstalled media codecs) by default.
# Functionally: Aside from missing media features, an “N” edition behaves exactly like the non-N version
computer_name: "*" # If you set <ComputerName>*</ComputerName> — the asterisk is a wildcard that instructs Windows: “Generate a random computer name automatically.
vm_admin_pass: "P@ssw0rd"
vm_time_zone: 'W. Europe Standard Time' #Time Zone
# Enable with: 'enabled=1,fstrim_cloned_disks=1,type=virtio'
agent: 'enabled=1,fstrim_cloned_disks=1,type=virtio'
Playbook file (e.g. playbook.yml) — the core “script” that defines what to do
The playbook describes what actions Ansible should perform on the hosts from the inventory — a list of “plays”, each comprising tasks.
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<settings pass="offlineServicing"></settings>
<!-- offlineServicing is the pass used when you are modifying or preparing a Windows image while it’s offline — i.e. not on a running system, but on an image or WIM mount. -->
<settings pass="windowsPE">
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<UILanguage>en-US</UILanguage>
</component>
<!-- WinPE Settings: The Core-WinPE component specifies the default language, locale, and other international settings to use during Windows Setup or Windows Deployment Services installations.
* loading provided virtio Driver
* running Disk Configuration
-->
<component name="Microsoft-Windows-PnpCustomizationsWinPE" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" processorArchitecture="amd64" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DriverPaths>
<PathAndCredentials wcm:action="add" wcm:keyValue="1">
<Path>e:\drivers\virtio\amd64</Path>
</PathAndCredentials>
</DriverPaths>
</component>
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<DiskConfiguration>
<WillShowUI>Never</WillShowUI>
<Disk wcm:action="add">
<DiskID>0</DiskID>
<WillWipeDisk>true</WillWipeDisk>
<CreatePartitions>
<CreatePartition wcm:action="add">
<Order>1</Order>
<Type>EFI</Type>
<Size>100</Size>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>2</Order>
<Type>MSR</Type>
<Size>128</Size>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>3</Order>
<Type>Primary</Type>
<Extend>true</Extend>
</CreatePartition>
</CreatePartitions>
<ModifyPartitions>
<ModifyPartition wcm:action="add">
<Order>1</Order>
<PartitionID>1</PartitionID>
<Label>System</Label>
<Format>FAT32</Format>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>2</Order>
<PartitionID>3</PartitionID>
<Label>Windows</Label>
<Letter>C</Letter>
<Format>NTFS</Format>
</ModifyPartition>
</ModifyPartitions>
</Disk>
</DiskConfiguration>
<!-- Image Install
* Image configuration from vars.yml
* preconfigured Product Key ( use your own )
-->
<ImageInstall>
<OSImage>
<WillShowUI>Never</WillShowUI>
<InstallTo>
<DiskID>0</DiskID>
<PartitionID>3</PartitionID>
</InstallTo>
<InstallToAvailablePartition>false</InstallToAvailablePartition>
<InstallFrom>
<MetaData wcm:action="add">
<Key>/IMAGE/NAME</Key>
<Value>{{deploy_image}}</Value>
</MetaData>
</InstallFrom>
</OSImage>
</ImageInstall>
<UserData>
<ProductKey>
<Key>2B87N-8KFHP-DKV6R-Y2C8J-PKCKT</Key>
<WillShowUI>OnError</WillShowUI>
</ProductKey>
<AcceptEula>true</AcceptEula>
</UserData>
<UseConfigurationSet>false</UseConfigurationSet>
</component>
</settings>
<!-- Generalize -->
<settings pass="generalize"></settings>
<!-- Speczialize Phase -->
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<ComputerName>{{computer_name}}</ComputerName>
<TimeZone>{{vm_time_zone}}</TimeZone>
</component>
</settings>
<settings pass="auditSystem"></settings>
<settings pass="auditUser"></settings>
<!-- out of the box experience aka oobe Phase -->
<settings pass="oobeSystem">
<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<InputLocale>0407:00000407</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<AutoLogon>
<Username>administrator</Username>
<Password>
<Value>{{vm_admin_pass}}</Value>
<PlainText>true</PlainText>
</Password>
<Enabled>true</Enabled>
<LogonCount>3</LogonCount>
</AutoLogon>
<OOBE>
<ProtectYourPC>3</ProtectYourPC>
<HideEULAPage>true</HideEULAPage>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
</OOBE>
<UserAccounts>
<AdministratorPassword>
<Value>{{vm_admin_pass}}</Value>
<PlainText>true</PlainText>
</AdministratorPassword>
</UserAccounts>
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<Order>1</Order>
<Description>Set Execution Policy 64 Bit</Description>
<CommandLine>cmd.exe /c powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force"</CommandLine>
<RequiresUserInput>true</RequiresUserInput>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>2</Order>
<Description>Set Execution Policy 32 Bit</Description>
<CommandLine>C:\Windows\SysWOW64\cmd.exe /c powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force"</CommandLine>
<RequiresUserInput>true</RequiresUserInput>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<CommandLine>%SystemRoot%\System32\reg.exe ADD HKLM\SYSTEM\CurrentControlSet\Control\Network\NewNetworkWindowOff /f</CommandLine>
<Order>3</Order>
<Description>Disable Network Discovery Prompt</Description>
</SynchronousCommand>
{% if agent is match("enabled=1") %}
<SynchronousCommand wcm:action="add">
<CommandLine>e:\drivers\virtio\amd64\qemu-ga-x86_64.msi /quiet</CommandLine>
<Order>4</Order>
<Description>Install qemu-guest-agent</Description>
</SynchronousCommand>
{% endif %}
<SynchronousCommand wcm:action="add">
<CommandLine>powershell -File e:\scripts\AnsiblePrep.ps1</CommandLine>
<Description>Configure Ansible Prep and WinRM</Description>
<Order>5</Order>
</SynchronousCommand>
</FirstLogonCommands>
</component>
</settings>
</unattend>Run the Playbook from your Ansible Instance
[root@Rocky2 ansible-win11-proxmox]# ansible-playbook -i inventory.yml --ask-vault-pass playbook.yml
Vault password:
PLAY [proxmox] ***********************************************************************************************************************
TASK [Get available VMID] ************************************************************************************************************
changed: [10.10.2.77]
TASK [Create temp directory] *********************************************************************************************************
ok: [10.10.2.77]
TASK [Template out autounattend answer file] *****************************************************************************************
ok: [10.10.2.77]
TASK [Copy files for ISO] ************************************************************************************************************
ok: [10.10.2.77]
TASK [Create ISO file] ***************************************************************************************************************
changed: [10.10.2.77]
TASK [Create VM] *********************************************************************************************************************
changed: [10.10.2.77]
TASK [Start VM] **********************************************************************************************************************
changed: [10.10.2.77]
TASK [Wait for VM to boot a little] **************************************************************************************************
Pausing for 8 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [10.10.2.77]
TASK [Click to boot from DVD] ********************************************************************************************************
changed: [10.10.2.77]
TASK [Wait for OS install, config, update, sysprep. Up to 120 min. Monitor status if possible.] **************************************
ASYNC POLL on 10.10.2.77: jid=j313960101483.141117 started=1 finished=0
ASYNC POLL on 10.10.2.77: jid=j313960101483.141117 started=1 finished=0
ASYNC POLL on 10.10.2.77: jid=j313960101483.141117 started=1 finished=0
..
ASYNC OK on 10.10.2.77: jid=j313960101483.141117
changed: [10.10.2.77]
TASK [Remove IDE CD Drives] **********************************************************************************************************
changed: [10.10.2.77]
TASK [Create template] ***************************************************************************************************************
changed: [10.10.2.77]
TASK [Remove Unattend ISO file] ******************************************************************************************************
changed: [10.10.2.77]
TASK [Cleanup temp files used for ISO build] *****************************************************************************************
changed: [10.10.2.77]
TASK [Done] **************************************************************************************************************************
ok: [10.10.2.77] => {
"msg": "Template build Win11, with VMID 105 complete."
}
PLAY RECAP ***************************************************************************************************************************
10.10.2.77 : ok=15 changed=10 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Windows Installation Stages in autounattended.xml
Here is a brief overview of what each “stage” (commonly referred to as a configuration pass) does.
What is a “configuration pass”
- Windows setup doesn’t run all configuration at once. Instead it runs in a sequence of passes (or phases).
- In your autounattend.xml file you specify which settings (components) should be applied in which pass.
- Not every pass has to be used — many automated installs just use a subset depending on how much automation you want.
| Pass | When it runs / what part of install | What you use it for |
| windowsPE | During the initial boot from the installation media — the “preinstallation environment” (PE) before Windows is copied to disk | * Region / language / locale and keyboard settings for setup. * Disk configuration — partitioning, formatting, selecting target drive. * Selecting Windows edition and entering product key (if required). * Accepting license agreement (EULA) and skipping initial setup dialogs. |
| offlineServicing | Before the first boot, while Windows image is still offline (i.e. not yet running) — often used for customizing an image before deployment | * Inject updates, patches, drivers or extra packages into the Windows image before first run. |
| generalize | Used when preparing a reference image (e.g. with Sysprep) to remove machine‑specific data so the image can be reused on other hardware. | * Remove unique identifiers (like SIDs), log history, restore points, hardware‑specific drivers, and other hardware‑/machine‑specific configs — so the image becomes “generic.” |
| specialize | First boot after Windows is copied, before user login — system‑level configuration before final user setup | * System-specific configuration: computer name, network settings, joining a domain/workgroup, registry tweaks. * Running scripts or commands (e.g. customizing OS, installing software, tweaks) via “RunSynchronous” or similar mechanisms. |
| auditSystem | Only used when install enters “audit mode” (rather than finishing setup immediately); runs under SYSTEM account before user logon | * System‑level customizations — for example additional driver installs or system tweaks — before any user logs in. |
| auditUser | Also only with audit mode — runs after user logon, under user context | * User‑level customizations: user-profile defaults, user‑context setup tasks, perhaps installing user‑specific software. |
| oobeSystem | When Windows reaches “out‑of‑box experience” (OOBE) — i.e. final user setup phase (account creation, initial settings), before first login | * Create user accounts, set up region/language preferences for user, configure auto‑login or first‑time login tasks. * Final polish: skip OOBE screens, disable default prompts, finalize privacy settings, run first‑login commands. |
Potential Challenges and How to Overcome Them:
1.) WARNING: Running pip as the ‘root’ user can result in broken permissions
See Virtual Environments — Why and When to Use Them
2.) TASK [Get available VMID] FAILED! => {“msg”: “to use the ‘ssh’ connection type with passwords or pkcs11_provider, you must install the sshpass program”}
[root@Rocky2 deploy]# dnf install sshpass
3.) Virtual Machines doesn’t boot / resets straight into Bios
Change CPU type to x86-64-v2-AES or x86-64-v3, depending on the CPU capabilities. There have been recent problems if CPU type is host.
Recommendations for KVM CPU model configuration on x86 hosts — QEMU documentation