- Windows PowerShell 5.1 or PowerShell 7, running as Administrator.
- Hyper-V enabled, its management tools installed, and a working
Default Switch(or changeswitch_namein the var-file). - The Windows ADK Deployment Tools.
The build script adds the standard
amd64andx86Oscdimg locations to its process PATH. Packer creates the unattended-install CD automatically; AnyBurn and manually mounting an answer ISO are not needed. - Packer, and Azure CLI if publishing. With Chocolatey already installed:
choco install packer azure-cli -yThe self-hosted GitHub Actions runner needs the same prerequisites and an appropriately privileged service account. Install Chocolatey on the runner once if the workflow should install missing Packer/Azure CLI packages. The workflow does not display UAC prompts or interactively enable Windows features.
Use the shared entry point rather than invoking packer build directly. It
initializes plugins, validates the effective configuration, builds the VM, and
verifies the exported disk. A failed native command stops subsequent stages.
git clone https://github.com/bobsira/windows-node-image-builder.git
Set-Location .\windows-node-image-builder
$result = .\scripts\Build-WindowsImage.ps1
$result.DiskPathOptional version overrides work for both validation and the build:
$result = .\scripts\Build-WindowsImage.ps1 `
-WindowsVersion '2022' `
-KubernetesVersion 'v1.37.0' `
-ContainerdVersion '1.7.25'Omitted or blank version overrides use windows.auto.pkrvars.hcl, not the latest
release. Use -VarFile for another var-file. The base VM name defaults to
hybrid-minikube-windows-server; -VmName overrides that base (and the var-file's
vm_name), but the unique build suffix is always appended.
To initialize and validate without creating a VM, taking the host build lock, or publishing:
.\scripts\Build-WindowsImage.ps1 -ValidateOnlyThe template sends the DVD boot key immediately and retries it ten times, rather
than relying on one delayed keystroke. Packer requires boot_wait = "-1s" to
disable the delay; "0s" selects its default ten-second wait. Installation then
uses setup\Autounattend.xml.
| Resource | Naming |
|---|---|
| GitHub build ID | <run-id>-<run-attempt> |
| Local build ID | UTC timestamp plus a random suffix |
| Temporary VM | hybrid-minikube-windows-server-<build-id> |
| Run directory | %ProgramData%\WindowsNodeImageBuilder\builds\<build-id> |
| Export directory | <run-directory>\output |
| Logs and result | <run-directory>\logs |
| Published disk | hybrid-minikube-windows-server.vhdx (or .vhd for an actual VHD) |
-ArtifactRoot overrides the parent directory of all run directories. The default
is outside the checkout so a later GitHub checkout cannot erase failed-run
evidence. -BuildId allows an explicit unique ID; reusing an existing directory
fails rather than overwriting it. Builds do not use -force.
Only the current run's exact export directory is searched. It must contain
exactly one nonempty, readable, detached, independent VHD/VHDX with a matching
extension. logs\result.json records the build status, VM name, exact disk path,
format, size, and retained VM details when available.
Both local and CI builds hold the same exclusive file lock at
%ProgramData%\WindowsNodeImageBuilder\build.lock. A competing build fails
explicitly. The lock is released when its owner exits; the file remaining on disk
does not mean it is still locked. Do not delete the lock file to bypass it.
Elevated Administrators and the runner's SYSTEM account must have access to this
shared directory; do not relocate the lock per user or checkout.
The entry point also rejects an already-running packer build for this template,
including older builds that did not acquire the lock. It never terminates them.
All new builds must use the shared entry point: raw Packer commands can bypass
the host lock, even though the template now requires a build ID and output path.
Local builds do not publish automatically. To publish a successful result, set
AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_KEY, and AZURE_CONTAINER_NAME in the
process environment using your normal secret-management mechanism, then run:
.\scripts\Publish-WindowsImage.ps1 `
-ResultPath (Join-Path $result.LogDirectory 'result.json')Publication rechecks the successful build result and its disk. The canonical
Azure blob name is independent of the temporary VM name. Local exported files
and VM configuration are not renamed; create or import the final VM with the
name hybrid-minikube-windows-server. Changing a .vhdx extension to .vhd is
not a disk-format conversion.
Publishers coordinate through a 60-second Azure lease on the canonical blob, renewed every 15 seconds during upload. This includes publishers on other hosts and local invocations. A competing publisher fails explicitly rather than waiting. The upload carries the lease ID, so a lost lease cannot commit over another publisher. Cleanup releases ownership; after a crashed publisher stops renewing, the finite lease expires. Do not break an active lease.
An absent destination is conditionally created as an empty block blob before
lease acquisition. A failed first publication can leave that zero-byte blob;
it is not a completed image. An existing image is never replaced by an empty
placeholder. Both .vhdx and .vhd are published as block-blob file artifacts,
not Azure VM page-blob disks.
Credentials are supplied through environment variables, not logged command-line
arguments. publication.json, publication.log, and publication-lease.log
are saved alongside the build result. The successful publication manifest and
verified nonempty disk distinguish a completed image from an initial placeholder.
The workflow uses actions/checkout@v7 and actions/upload-artifact@v7, which
natively run on Node.js 24. Self-hosted runners require Actions Runner 2.327.1
or newer. The Windows/Hyper-V runner reported 2.337.0 in
run 34721519634,
meeting that requirement. Checkout's separate credential storage requires
2.329.0 or newer for authenticated Git commands inside Docker container actions;
this workflow does not use those actions.
See the upstream checkout migration notes and upload-artifact release notes. Checkout's new fork-PR restrictions do not affect this workflow's push/manual triggers. Artifact upload keeps the default ZIP mode, preserving directory uploads and the explicit per-run artifact name.
After upgrading, confirm checkout, image build, canonical Azure publication, and
log upload in a new workflow run. The Node.js 20 runtime-forcing warning should
be absent. Upload-artifact v6 included a punycode deprecation fix, inherited by
v7; whether either punycode or url.parse() warnings remain must still be
checked in the new run. Record any remaining warnings with the run URL rather
than suppressing them or opting back into Node.js 20.
The workflow calls the same build and publication scripts. Its optional version inputs retain the var-file defaults when left blank. A branch-independent concurrency group prevents this repository's workflows from overlapping without cancelling the active build. The host lock also covers local builds, and the storage lease protects publication across hosts.
Publication runs only after a successful build. The workflow always attempts to
upload the run's logs as packer-log-<run-id>-<run-attempt>, including publication
diagnostics when that stage ran. If a failure happens before the build entry
point can create its log directory, inspect the workflow step's own log.
Kubeadm and kubelet downloads are verified against the release's SHA-256 checksum before replacing the destination executable. Each download attempt uses a 300-second timeout per request, with up to three attempts and 5/10-second retry delays. Failed attempts log exception details and HTTP status when available. Partial files are removed, and exhausting retries terminates provisioning rather than exporting an image with a missing or corrupt binary.
Packer runs with -on-error=abort. The failed VM and its associated files are
preserved, and a new run cannot reuse their unique identity. Logs include
console.log (native stdout/stderr), packer-debug.log, host.log, and
result.json. Validation-only results are marked Validated, never Succeeded,
and cannot be published.
Before retrying, inspect the error and any active workflow/Packer process. For boot failures, inspect the VM console and DVD boot prompt. For provisioning failures, check the exact retained VM's IP, WinRM listener on TCP 5985, and guest events at the failure time. Two controllers acting on the same VM can cause download file locks and competing restarts.
After collecting diagnostics and confirming no build is using the resource,
remove only the failed VM identified by RetainedVM.Id and the exact associated
paths recorded in its result. VM disks may still be in Packer's temporary build
directory rather than the export directory. Do not delete other VMs, broad
output* paths, the shared build root, or an active lock. Logs can contain
sensitive machine details; keep their filesystem and artifact access restricted.
Run the test suites with one command (Pester 4.9.0 must be installed):
.\scripts\Test-WindowsImage.ps1The runner imports the required Pester version and finds the tests relative to
its own location, so it also works when invoked by absolute path from another
directory. It prints the test results and returns exit code 0 on success or 1
on test failure, missing tests, or a runner error. To run it in a fresh Windows
PowerShell process:
powershell.exe -NoProfile -File .\scripts\Test-WindowsImage.ps1Check Packer formatting separately:
packer fmt -check .\windows.json.pkr.hclThe script tests use mocks for provisioning and Azure operations; they do not start a VM or publish an image.
| OS | Username | Password |
|---|---|---|
| Windows | Administrator | password |