6.9 KiB
Vagrant box build (Packer + Hyper-V + Windows 11 25H2)
Builds a bootstrap-win11.box Vagrant box from a stock Windows 11 25H2 ISO,
ready to use with test/integration/Vagrantfile.
Prerequisites
On the host (must be Windows with Hyper-V):
-
Hyper-V role enabled; current user in Hyper-V Administrators.
-
HashiCorp Packer 1.15+ in
PATH. HashiCorp does not publish a winget manifest, so use the helper:# from an elevated PowerShell cd test\integration\packer .\install-packer.ps1 # installs the pinned default version .\install-packer.ps1 -Version 1.14.0 # or pick a specific releaseThe script downloads
https://releases.hashicorp.com/packer/<Version>/packer_<Version>_windows_amd64.zip, extractspacker.exetoC:\Program Files\Packer\, and adds that directory to the machine-scopedPATH. It is idempotent — re-running with the same version just re-asserts thePATHentry. See theparam()block ininstall-packer.ps1for available flags.By default the same script also installs Windows ADK Deployment Tools to get
oscdimg.exe, which Packer'shyperv-isobuilder needs to build the unattended-install secondary ISO fromcd_files. Only theOptionId.DeploymentToolsfeature is selected, so the install is small (~70 MB) and unattended. Pass-SkipOscdimgto opt out (e.g. if you already havexorriso/mkisofsonPATH). After install you may need to open a fresh shell — or run$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine')— foroscdimg.exeto resolve in the current session. -
HashiCorp Vagrant 2.4+ in
PATH(used for thevagrantpost-processor and, later, the integration test itself). -
A Windows 11 client ISO somewhere on disk. Its path is read from
.env(see below) and is not hardcoded in the Packer config. -
Roughly 90 GB free disk and ~60 minutes of patience for a first build.
-
An elevated PowerShell session for the build (
packer buildshells out to Hyper-V management APIs that require admin).
Configure the ISO path via .env
The Packer config has no default ISO path. Copy the template, fill it in, and load it into the current shell before building:
cd test\integration\packer
Copy-Item .env.example .env
# edit .env so PKR_VAR_iso_path points at your Windows 11 ISO
. .\load-env.ps1
load-env.ps1 reads .env line-by-line and exports each KEY=VALUE into the
process environment. .env is gitignored at the repo root, so your local path
never gets committed.
Packer picks the values up automatically through its PKR_VAR_<name> env-var
convention; nothing about the packer build invocation changes.
One-time setup
packer init .\windows-11.pkr.hcl
That downloads the hyperv and vagrant Packer plugins.
Compute and pin the ISO checksum (recommended)
Get-FileHash $env:PKR_VAR_iso_path -Algorithm SHA256
Set PKR_VAR_iso_checksum=sha256:<digest> in .env and re-run load-env.ps1.
Leaving it as none skips validation; fine for local builds but loses the "my
ISO has not been swapped" guarantee.
Build
packer build .\windows-11.pkr.hcl
Roughly what happens:
- Packer creates a Gen 2 Hyper-V VM (Secure Boot + TPM + 24 vCPU + 32 GB RAM
- 80 GB dynamic VHDX) attached to
Default Switch. The build VM is throwaway and intentionally claims most of the host for speed; the runtime integration VM uses smaller defaults set intest/integration/Vagrantfile(8 vCPU / 16 GB).
- 80 GB dynamic VHDX) attached to
- It mounts the Windows ISO and a tiny
PROVISIONISO containingcd/autounattend.xmlandcd/scripts/oobe-enable-winrm.ps1. - Windows Setup performs an unattended install (Pro edition, generic
activation key) and creates user
vagrant:vagrant. - FirstLogonCommands enable WinRM (HTTP, basic auth) and set
LocalAccountTokenFilterPolicy=1so remote admin tokens are not filtered. - Packer connects over WinRM and runs the provisioners in
scripts/:configure-os.ps1— UAC token policy, DiagTrack off, NTP resync.disable-windows-updates.ps1— fully disables WU and friends.disable-defender-cloud.ps1— disables MAPS / cloud lookups.install-vagrant-key.ps1— drops the vagrant insecure public key.compact.ps1— cleans caches, zeros free space.
- Packer shuts the guest down cleanly and the
vagrantpost-processor packages the VHDX intobootstrap-win11.box.
Register the resulting box with Vagrant
vagrant box add bootstrap-win11 .\bootstrap-win11.box
Then in test/integration/Vagrantfile set:
$env:BOOTSTRAP_BOX = "bootstrap-win11"
go run ..\..\test\integration
(or just edit the default in the Vagrantfile to bootstrap-win11).
Layout
test/integration/packer/
├── README.md ← this file
├── .env.example ← template; copy to .env (gitignored)
├── load-env.ps1 ← dot-source to export PKR_VAR_* into shell
├── install-packer.ps1 ← elevated installer for packer.exe
├── windows-11.pkr.hcl ← Packer HCL2 build definition
├── vagrantfile-template.rb ← baked into the output box
├── cd/
│ ├── autounattend.xml ← Windows Setup answer file
│ └── scripts/
│ └── oobe-enable-winrm.ps1
└── scripts/ ← provisioners run after WinRM is up
├── configure-os.ps1
├── disable-windows-updates.ps1
├── disable-defender-cloud.ps1
├── install-vagrant-key.ps1
└── compact.ps1
Notes and gotchas
- Run elevated.
packer buildopens Hyper-V management APIs; non-elevated shells fail with confusing "access denied" messages midway through. - First build is slow. Windows Setup + updates trimming + cipher /w on a 60 GB volume can take 45–90 min. Subsequent rebuilds reuse the parent VHDX via linked clone and are much faster.
- License compliance. The box uses the public KMS client setup key (W269N-WFGWX-YVC9B-4J6C9-T83GX) to pick the Pro edition during install. The resulting VM is not activated; for short-lived integration runs the eval period is more than sufficient, but redistribute the box only under your own licensing terms.
- 25H2 OOBE. The autounattend sets
BypassNRO=1duringspecializebecause 24H2/25H2 removed theBypassNRO.cmdhelper. If a future Windows update changes the OOBE flow again, the symptom will be a hungvagrant upwaiting for WinRM; check by opening Hyper-V Manager and looking for an OOBE screen on the VM console. - Secure Boot. Enabled (
MicrosoftWindowstemplate). Switch off in the Packer source if you ever need to install unsigned kernel-mode drivers in the box.