You are viewing documentation for Cozystack next, which is currently in beta. For the latest stable version, see the v1.6 documentation.
Building a Windows Golden Image with Packer
Running Windows VMs shows how to boot and install a Windows VM by hand (including the Virtio drivers). This guide is the automated counterpart: it bakes a Windows Server install plus your software and configuration into one reusable image with Packer, so every VM clones from a prepared disk instead of being installed from scratch.
The
Golden Images guide covers named images cached from an HTTP(S) URL — the vm-default-images collection and custom entries added with cdi_golden_image_create.sh. That model fits cloud images that already exist as a single downloadable disk (most Linux cloud images do). A customized Windows image cannot be expressed as a plain download URL, so you build it with Packer against the KubeVirt builder, capture the configured disk, and register that disk instead.
When to use this
- The image needs software or configuration baked in (an application, drivers, agents, autologon), so a bare cloud-image URL will not do.
- An installer is interactive or licensed and cannot be scripted end-to-end.
- You want one prepared Windows disk that many VMs clone from, instead of re-installing per VM.
For a bare, unmodified base that is reachable by URL, prefer the simpler
vm-default-images path instead.
Prerequisites
packer.- A
KUBECONFIGpointing at the target cluster, with access to a tenant namespace (e.g.tenant-root). - The Windows installation ISO staged as a DataVolume, so the builder can boot from it:
kubectl apply -f win2022-iso-dv.yaml
The KubeVirt builder plugin
Packer creates the VM on the cluster with the KubeVirt builder — the published plugin
github.com/hashicorp/kubevirt, which provides the kubevirt-iso builder for both Linux and Windows. Declare it in required_plugins (see the template below) and install it with packer init — no manual download.
The plugin repository has a
Windows kubevirt-iso example that this guide follows.
The Packer template
A minimal windows.pkr.hcl has three parts: the plugin requirement, a source describing the VM and how Packer connects to it (WinRM), and a build listing the provisioners.
packer {
required_plugins {
kubevirt = {
# Published plugin — `packer init` installs it automatically.
source = "github.com/hashicorp/kubevirt"
version = ">= 0.9.0"
}
}
}
source "kubevirt-iso" "windows" {
kube_config = var.kube_config
name = var.image_name
namespace = var.namespace
iso_volume_name = "windows-2022-x86-64-iso" # the DataVolume you applied above
disk_size = "32Gi"
instance_type = "u1.large"
instance_type_kind = "virtualmachineclusterinstancetype"
preference = "windows.2k22.virtio"
preference_kind = "virtualmachineclusterpreference"
os_type = "windows"
networks {
name = "default"
pod {}
}
# Files placed on the setup CD. Windows Setup auto-reads autounattend.xml;
# the scripts run at first boot and enable WinRM so Packer can connect.
media_files = [
"./autounattend.xml",
"./scripts/enable-winrm.ps1",
"./scripts/set-network.ps1",
]
boot_command = ["<wait1>"] # press a key to boot from the install CD
boot_wait = "5s"
installation_wait_timeout = "20m"
communicator = "winrm"
winrm_host = "127.0.0.1"
winrm_local_port = 5000
winrm_remote_port = 5985
winrm_username = "Administrator"
winrm_password = var.build_password # must match autounattend.xml
winrm_wait_timeout = "45m"
}
build {
sources = ["source.kubevirt-iso.windows"]
# Confirm the WinRM connection is up.
provisioner "powershell" {
inline = ["(Get-CimInstance Win32_OperatingSystem).Caption"]
}
# Your software + guest configuration — replace with what you need baked in.
provisioner "powershell" { script = "./scripts/install-software.ps1" }
provisioner "powershell" { script = "./scripts/configure-guest.ps1" } # RDP, disable sleep, etc.
provisioner "powershell" { script = "./scripts/configure-autologon.ps1" } # optional: logged-in desktop on boot
# FINAL provisioner — keep it last. Reverts the build-time WinRM/RDP
# relaxations and strips any baked autologon password, so nothing weakened
# from the build ships in the golden.
provisioner "powershell" { script = "./scripts/harden.ps1" }
}
Declare the variables in variables.pkr.hcl:
variable "kube_config" {
type = string
default = "${env("KUBECONFIG")}"
}
variable "namespace" {
type = string
default = "tenant-root"
}
variable "image_name" {
type = string
default = "windows"
}
variable "build_password" {
type = string
sensitive = true
default = "REPLACE_ME_BUILD_PW" # throwaway; must match autounattend.xml
}
The answer file (autounattend.xml)
Windows Setup reads autounattend.xml from the media CD and installs unattended. Two things matter for the build:
- It sets the Administrator password to the same value as
var.build_password— that is how Packer’s WinRM communicator authenticates. - It runs
enable-winrm.ps1at first logon, which opens WinRM (basic + unencrypted, port 5985) so Packer can connect. These relaxations are temporary —harden.ps1reverts them before the golden is captured.
The password in autounattend.xml is a build-time placeholder, not a real secret — see the note below.
Run the build
export KUBECONFIG=/path/to/kubeconfig
export PKR_VAR_build_password='<throwaway-build-password>' # must match autounattend.xml
packer init . # installs the KubeVirt builder plugin
packer build .
Packer boots the VM from the ISO, waits for WinRM, runs the provisioners in order, and shuts the VM down. If any of your installers is not silent (cannot be scripted), install it interactively instead: RDP into the running VM (autologon gives you a logged-in desktop), install and verify through its GUI, then sign out and let the build continue to harden.ps1. When the build finishes, capture the VM disk as the golden.
Never bake credentials or licences
The build-time Administrator/WinRM password is a throwaway placeholder inautounattend.xml and variables.pkr.hcl — change it per build and reset it at deploy. The autologon password is not baked: inject it at deploy time or use Sysinternals Autologon.exe (which stores it as an encrypted LSA secret). Any application credentials are entered by hand in the running VM, never committed to the image or to Git.Sysprep on evaluation media
On the Windows Server evaluation ISO,sysprep /generalize can crash (spopk.dll). If it does, capture the golden without generalize — but then clones share the same SID and computer name, which is acceptable only for a single VM; assign a unique name per clone if you run several. A purchased licence and non-evaluation media are required for production use.Register the image in Cozystack
Once you have a captured Windows disk, make it reusable one of two ways:
- As a named image you clone per VM (recommended for customized disks). Capture the prepared disk into a
vm-image-<name>DataVolume in thecozy-publicnamespace, then create eachVMDiskfrom it viasource.image.name— the full flow is in Cloneable Virtual Machines. This is the right choice for a customized Windows image, whose disk is not a single public URL. The clone strategy (a storage smart-clone vs a host-assisted copy) is chosen by CDI and the storage backend, not set in the manifest; on some backends a smart-clone can reportSucceededwhile leaving the target empty, so verify the cloned DataVolume both reachesSucceededand actually contains data before relying on it. If it comes up empty, set the annotationcdi.kubevirt.io/cloneType: copyon thevm-image-<name>DataVolume to force a host-assisted, byte-for-byte copy — it either populates the target or fails loudly instead of silently succeeding. - As a
vm-default-imagescollection entry. This path caches an image from a public HTTP(S) URL — see Golden Images. It suits a bare base image reachable by URL, not a customized captured disk.
Create and access the VM
Create a VMInstance from a cloned Windows disk, setting the Windows preference and an instance type explicitly (otherwise the VM defaults to the ubuntu profile and boots Windows with the wrong devices and scheduling):
kubectl -n tenant-root create -f- <<EOF
apiVersion: apps.cozystack.io/v1alpha1
kind: VMInstance
metadata:
name: windows
spec:
instanceProfile: windows.2k22.virtio
instanceType: u1.large
disks:
- name: windows
EOF
Access the desktop over VNC or an RDP port-forward:
virtctl vnc vm-instance-windows -n tenant-root
# or forward RDP (3389) and connect with an RDP client
Autologon lands you on a ready desktop.
Stopping the VM
Stop a Windows VM built this way withvirtctl stop --force — a graceful ACPI stop does not always work reliably on it.