We are going to run a Windows VM inside of Kubernetes. This will be a long one.

We will do this by;

Once done, we can create Windows VMs on demand in Kubernetes.

Why? Well, imagine you want to run multiple old-school services, that are not easily containerizable. Say an IIS website with a SQL Server and some weirdo services that don’t work in a Windows container image. We can use this to host an instance per client on one large node pool instead of individual VMs and benefit from the Kubernetes ecosystem. Or, create many Dev machines that can easily be created and deleted on demand. We can do this as if they were pods!

Note: You probably need a pretty powerful machine to do this. I tried it on an Azure 4 CPU/16GB RAM machine and it kinda melted. A real desktop-style machine with an NVME drive is highly recommended due to the strain of creating the image and deploying it into a KIND cluster.

Required Files

Because of the complexity of the full setup, I’ve put all the files into a GitHub repo, links below. Where useful I’ll share a snippet. But it may be best to pull the zip/repo from Git Hub and follow along as the code would make this blog post very large and unwieldy.

Repository - https://github.com/rootisgod/Kubevirt-Cluster

Zip File - https://github.com/rootisgod/Kubevirt-Cluster/archive/refs/heads/main.zip

Creating a VM Image

To get to the stage of running a Windows machine in Kubernetes, we need a VM image. And for that, we need Virtualbox to create a VM, and Packer to make the image from it. And later qemu-img program to convert the VM file.

Note: For this guide, we are using Windows as the base OS to show the steps. It doesn’t change things too much if using Linux, but worth noting.

Installing Packer and Virtualbox

The easiest way to install Virtualbox and Packer is with Chocolatey (or install both programs manually if you know what you are doing). You can install Chocolatey with these instructions - https://chocolatey.org/install To install the programs, run this.

choco install virtualbox packer qemu-img -y

Windows ISO

We also need a Windows Server 2022 ISO. You can grab an evaluation licence ISO from here: https://www.microsoft.com/en-gb/evalcenter/download-windows-server-2022. I have placed it into a folder on my computer called D:\ISOs\windows_server_2022.iso. Update the location in the code further on in file windows.pkr.hcl and variable iso_url, with wherever yours is located.

Packer

Now we can think about deploying it with Packer. But, first, we need to install the following packer plugins so it can talk to Virtualbox, like so.

packer plugins install github.com/hashicorp/vagrant
packer plugins install github.com/hashicorp/virtualbox

Packer Windows 2022 Template

We can now create a VM with a packer template. The template is responsible for the full lifecycle of the image we create. It will create a VM in Virtualbox, install Windows via an answer file, and then connect to it over WinrRM to configure it and then shut it down. Once this happens it will output it as an image we can use later. So we need to give it quite a lot of information and scripts to make that happen.

This is the template we need to run. It has teh VM spec and our build options. It should be pretty simple to understand. Tweak the values if you wish. Save it as a file called windows.pkr.hcl. It is also here

packer {
  required_plugins {
    vagrant = {
      source  = "github.com/hashicorp/vagrant"
      version = "~> 1"
    }
    virtualbox = {
      source  = "github.com/hashicorp/virtualbox"
      version = "~> 1"
    }
  }
}

source "virtualbox-iso" "windows" {
  vm_name              = "win2022"
  communicator         = "winrm"
  floppy_files         = ["files/Autounattend.xml", "scripts/enable-winrm.ps1", "scripts/sysprep_and_shutdown.bat", "scripts/shutdown.bat"]
  guest_additions_mode = "attach"
  guest_os_type        = "Windows2022_64"
  headless             = "false"
  iso_checksum         = "sha256:3e4fa6d8507b554856fc9ca6079cc402df11a8b79344871669f0251535255325"
  iso_url              = "d:/ISOs/windows_server_2022.iso"
  disk_size            = "24576"
  shutdown_timeout     = "15m"
  vboxmanage           = [["modifyvm", "{{ .Name }}", "--memory", "8192"], ["modifyvm", "{{ .Name }}", "--vram", "48"], ["modifyvm", "{{ .Name }}", "--cpus", "4"]]
  winrm_password       = "vagrant"
  winrm_timeout        = "12h"
  winrm_username       = "vagrant"
  keep_registered      = "false"
  # shutdown_command     = "a:/sysprep_and_shutdown.bat"
  shutdown_command     = "a:/shutdown.bat"
}

build {
  sources = ["source.virtualbox-iso.windows"]

  provisioner "powershell" {
    elevated_password = "vagrant"
    elevated_user     = "vagrant"
    script            = "scripts/customise.ps1"
  }

  # Add other script you want to run here, like Windows Updates, software installs etc...

  provisioner "windows-restart" {
    restart_timeout = "15m"
  }
}

And we need a few files in a couple of folders to take care of an unattended install, and some post-boot actions.

Scripts

The most important file is the enable-winrm.ps1 file. The answer file (below) references this and will run it once Windows is installed. It sets up winrm so that Packer can send commands to it once the OS is installed.

Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private
Enable-PSRemoting -Force
winrm quickconfig -q
winrm quickconfig -transport:http
winrm set winrm/config '@{MaxTimeoutms="1800000"}'
winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="800"}'
winrm set winrm/config/service '@{AllowUnencrypted="true"}'
winrm set winrm/config/service/auth '@{Basic="true"}'
winrm set winrm/config/client/auth '@{Basic="true"}'
winrm set winrm/config/listener?Address=*+Transport=HTTP '@{Port="5985"}'
netsh advfirewall firewall set rule group="Windows Remote Administration" new enable=yes
netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes action=allow remoteip=any
Set-Service winrm -startuptype "auto"
Restart-Service winrm

We also need a sysprep_and_shutdown.bat and shutdown.bat file in a scripts folder to shutdown simply or Sysprep it to ‘randomise’ the VM on boot (both are useful). Use the one you prefer in the packer template. But a simple shutdown might be preferable initially to avoid the ‘new user’ login screen while testing.

sysprep_and_shutdown.bat

c:\windows\system32\sysprep\sysprep.exe /generalize /mode:vm /oobe 
shutdown /s

shutdown.bat

shutdown /s

And we also need a customise.ps1 script to configure some small settings, and install Chocolatey and virtio drivers. Add/amend as you require. Choco being pre-installed is useful as you can add anything post-build very easily, or create another packer template provisioner section to add more software in a simple way.

# Set some Quality of Life Settings
c:\Windows\System32\reg.exe ADD HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\ /v HideFileExt /t REG_DWORD /d 0 /f
c:\Windows\System32\reg.exe ADD HKCU\Console /v QuickEdit /t REG_DWORD /d 1 /f
c:\Windows\System32\reg.exe ADD HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\ /v Start_ShowRun /t REG_DWORD /d 1 /f
c:\Windows\System32\reg.exe ADD HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\ /v StartMenuAdminTools /t REG_DWORD /d 1 /f
c:\Windows\System32\reg.exe ADD HKLM\SYSTEM\CurrentControlSet\Control\Power\ /v HibernateFileSizePercent /t REG_DWORD /d 0 /f
c:\Windows\System32\reg.exe ADD HKLM\SYSTEM\CurrentControlSet\Control\Power\ /v HibernateEnabled /t REG_DWORD /d 0 /f
c:\Windows\System32\reg.exe add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0 /f
netsh advfirewall firewall set rule group="remote desktop" new enable=yes

# Install Chocolatey
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

# Chocolatey Seems to cause a non-zero exit, cause a 500MB download, exits with a non-zero code and breaks the build... Lets install ourselves
$url  = 'https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/latest-virtio/virtio-win-guest-tools.exe'
$dest = 'c:\virtio-win-guest-tools.exe'
Invoke-WebRequest -Uri $url -OutFile $dest
c:\virtio-win-guest-tools.exe -s

Files

And we need an answer file for Windows to skip the install questions. Importantly, this also references and runs the enable-winrm.ps1 script. It also makes a user called vagrant with password vagrant and auto-logins the account. Not entirely secure, but you could remove this account later and tweak as required once you understand it all.

The answer file is very large, so I’m just showing the winrm script portion here so you know how the magic happens. Virtualbox mounts it to the A: so Windows can read it in and set up the remote access for us.

The full file is here here

```Autounattend.xml`Okay, that’s a lot. But you should effectively have this folder structure

windows.pkr.hcl
Scripts\enable-winrm.ps1
Scripts\sysprep_and_shutdown.bat
Scripts\shutdown.bat
Scripts\customise.ps1
Files\Autounattend.xml

Building the Image

We can now build a Windows image using Packer and Virtualbox!

Run this command

packer build ./windows.pkr.hcl

It will whirr away and automatically create a Virtualbox VM, then show a console of the build, and then shut the VM down and export a VDI file in an output-windows folder. Just leave it alone and it will shut down automatically.

NOTE: If you get a checksum error, then the packer output should show what it got and what it expects, simply change the windows.pkr.hcl variable iso_checksum to what it should be.

The terminal should log all this is going on.

Then, once completed, we can convert the file into a format required for Kubevirt like so. Create a QCOW folder somewhere to hold the output file we get.

qemu-img convert -f vmdk -O qcow2  ./output-windows/win2022-disk001.vmdk D:/QCOW/windows-2022.qcow2

KIND

We now have an image to run in Kubevirt, but first we need a Kubernetes cluster. We will use KIND to run the VM as it is supported by the Kubevirt project, and can run on Windows using Docker Desktop and WSL2.

Installation of WSL2 and Docker Desktop for KIND

It could be as simple as this though to install KIND, but your mileage may vary. If you already have Docker Desktop (https://www.docker.com/products/docker-desktop/) and WSL2 (https://kind.sigs.k8s.io/docs/user/using-wsl2/) installed then I’d avoid it and install KIND manually. See these instructions if either of the below fails: https://kind.sigs.k8s.io/docs/user/quick-start

Manual way (find a better path folder if you don’t want to dump it in System32)

curl.exe -Lo kind-windows-amd64.exe https://kind.sigs.k8s.io/dl/v0.23.0/kind-windows-amd64
Move-Item .\kind-windows-amd64.exe c:\windows\system32\kind.exe

If you don’t have WSL2 and Docker Desktop already, you can try this. It will install KIND and the dependencies.

choco install kind -y

WSL2 Tweak

There is also a tweak in WSL2 we need to perform. The default allocated memory for WSL2 for Docker will likely not be enough, so stop WSL and create/amend your users .wslconfig file to something like the below

# turn off all wsl instances such as docker-desktop
wsl --shutdown
notepad "$env:USERPROFILE/.wslconfig"
[wsl2]
memory=8GB   # Limits VM memory in WSL 2 up to 8GB
processors=4 # Makes the WSL 2 VM use more virtual processors

Then restart Docker desktop from its GUI.

We are getting there!

Setup Kubevirt

We should have the required basic tools installed and can now create a KIND cluster to host our VMs.

There is a quickstart guide here: https://kubevirt.io/quickstart_kind/

But this is what we will do.

Create a file called kind_config.yml. It will help us with a NodePort service we create later.

# https://stackoverflow.com/questions/62432961/how-to-use-nodeport-with-kind
apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
    listenAddress: "0.0.0.0" # Optional, defaults to "0.0.0.0"
    protocol: tcp # Optional, defaults to tcp

And install kubectl

choco install kubernetes-cli -y

Then create our cluster like so

kind create cluster --name kubevirt --config=kind_config.yml
kubectl cluster-info --context kind-kubevirt

We should get our cluster info. KIND will automatically make this our Kubernetes context.

Kubernetes control plane is running at https://127.0.0.1:58905
CoreDNS is running at https://127.0.0.1:58905/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

And now we can install Kubevirt into the cluster. Kubevirt is what allows our cluster to become a hypervisor. There is a guide here: https://kubevirt.io/quickstart_kind/

These are the commands I used, which have hard coded the versions for simplicity

#  https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt
kubectl create -f https://github.com/kubevirt/kubevirt/releases/download/v1.2.2/kubevirt-operator.yaml
kubectl create -f https://github.com/kubevirt/kubevirt/releases/download/v1.2.2/kubevirt-cr.yaml

Check it works, the outputs should be ‘Deployed’ and various things should be ‘Running’

kubectl get kubevirt.kubevirt.io/kubevirt -n kubevirt -o=jsonpath="{.status.phase}"
kubectl get all -n kubevirt

We should get a bunch of Kubevirt resources like below (more than shown here)

NAME                                   READY   STATUS    RESTARTS        AGE
pod/virt-api-75859b7b7-dn4sd           1/1     Running   5 (4d11h ago)   7d18h
pod/virt-controller-6855b4df79-4m7rn   1/1     Running   5 (4d11h ago)   7d18h
pod/virt-controller-6855b4df79-tzk5v   1/1     Running   5 (4d11h ago)   7d18h
...

Setup Virtctl

Then we install Virtctl to control Kubevirt VMs, much like kubectl. Grab the latest version here and move it into a system32 folder so it can be seen in our terminal (it’s not in Chocolatey…).

curl.exe -Lo virtctl.exe https://github.com/kubevirt/kubevirt/releases/download/v1.2.2/virtctl-v1.2.2-windows-amd64.exe
mv virtctl.exe c:\windows\system32\virtctl.exe

Setup a CDI

We also need a CDI (Containerized Data Importer) operator. This is the mechanism that we use to set up our VM disk images for Kubevirt to create our VMs. We can install that to our cluster like so.

kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/v1.59.0/cdi-operator.yaml
kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/v1.59.0/cdi-cr.yaml

Then, wait a minute or so and we should have some resources deployed and pods running.

kubectl get cdi cdi -n cdi
kubectl get pods -n cdi

Now, we can (almost) finally install and manage VMs with Kubernetes.

But wait… We need a place to host the QCOW2 file we created that Kubernetes can get to, and a web server is easiest (Perhaps there is a way to host it in the cluster, but I’m not sure).

We can use a portable web server, but let’s avoid the messiness of Python and use a go binary. This isn’t production-ready, but fine for our needs. Download the zip and extract it to your local folder (or C:\Windows\System32). The -g switch turns off logging, and the -l means show logs. The D:\QCOW path is where our qemu-convert image we made earlier should be.

https://github.com/m3ng9i/ran/releases/download/v0.1.6/ran_windows_amd64.exe.zip

curl.exe -Lo ran_windows_amd64.exe.zip https://github.com/m3ng9i/ran/releases/download/v0.1.6/ran_windows_amd64.exe.zip
Expand-Archive .\ran_windows_amd64.exe.zip
mv .\ran_windows_amd64.exe\ran_windows_amd64.exe D:\QCOW\ran.exe
D:\QCOW\ran.exe -r C:\QCOW\ -l -g false  

Then, we can reference the image like this (use your IP obviously): http://192.168.1.108:8080/windows-2022.qcow2. You should see the files in a browser like this.

The Kubevirt YAML files need to know where this web server is, and the LAN IP so it can find the image to download from inside the KIND cluster. Amend the kubevirt_win2022_dv.yml file to your machine IP (or the machine hosting the site) like below, and create these files with the names referenced.

kubevirt_win2022_dv.yml

apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: "kubevirt-win2022"
  labels:
    # insert any desired labels to identify your claim
    app: win2022
spec:
  storage:
    accessModes:
      - ReadWriteOnce
    resources:
      requests:
        storage: 52Gi
  source:
    http:
      url: "http://192.168.1.108/QCOW/windows-2022.qcow2"

kubevirt_win2022_pvc.yml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: win2022
  labels:
    # insert any desired labels to identify your claim
    app: win2022
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      # The amount of the volume's storage to request
      storage: 64Gi

kubevirt_win2022_vm.yml

apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  creationTimestamp: 2018-07-04T15:03:08Z
  generation: 1
  labels:
    kubevirt.io/os: windows
  name: win2022-vm
spec:
  running: true
  template:
    metadata:
      creationTimestamp: null
      labels:
        kubevirt.io/domain: win2022-vm
    spec:
      domain:
        cpu:
          cores: 2
        resources:
          requests:
            memory: 4096M
        firmware:
          uuid: 5d307ca9-b3ef-428c-8861-06e72d69f223
        devices:
          disks:
          - disk:
              bus: sata
            name: disk0
        machine:
          type: q35
      volumes:
      - name: disk0
        persistentVolumeClaim:
          claimName: kubevirt-win2022

kubevirt_win2022_svc.yml

apiVersion: v1
kind: Service
metadata:
  name: win2022-vm-nodeport
spec:
  externalTrafficPolicy: Cluster
  ports:
  - name: nodeport
    nodePort: 30000
    port: 27017
    protocol: TCP
    targetPort: 3389
  selector:
    kubevirt.io/domain: win2022-vm
  type: NodePort

Then we can actually deploy a VM!

Creating a VM

Create the required Persistent Volume Claim and Data Volume.

kubectl apply  -f kubevirt_win2022_pvc.yml
kubectl apply  -f kubevirt_win2022_dv.yml

Then, create the VM

kubectl apply  -f kubevirt_win2022_vm.yml

We can check the status with this command. But, it will take a long time for the QCOW2 image to be shuffled into the Kubernetes cluster, so be patient, and keep an eye on your disk IO and CPU. It should be running 100% CPU while it is provisioning the VM data (passing a 10GB image from a website to a Docker container, into a Kubernetes Cluster is resource intensive).

kubectl describe vm win2022-vm

And then we can see if it is alive! Run this command and you will, eventually, see a VNC screen with your Windows VM.

virtctl vnc win2022-vm

Also, apologies, you probably need to install vncviewer.exe to use the vnc command. I believe I downloaded TightVNC (https://www.tightvnc.com/download/2.8.84/tightvnc-2.8.84-gpl-setup-64bit.msi), installed it, and then copied the file C:\Program Files\TightVNC\tvnviewer.exe to c:\windows\system32\vncviewer in order to get it to work. Note I changed tvnviewer.exe to vncviewer.exe.

RDP to the VM

To RDP into the machine, we can set up a service. Run this command.

kubectl apply  -f kubevirt_win2022_svc.yml

What it will do is set up a Nodeport connection over port 30000. So, to RDP we just have to RDP to our machine’s localhost port and port 30000 (remember the bit at the start with our kind config file, that was us exposing that port).

And if you are on an external machine, you can RDP to the Kubevirt Windows VM using the server ip and port 30000, awesome.

Taskfiles

To try and tame the complexity of the many commands, I created a Taskfile to simplify things. Taskfile is like a modern implementation of Make and is available for all OS’s as a single binary file to install. This file is for Windows machines, but the tasks should be amendable for Linux or Mac if required. We can install Taskfile like this (or see here: https://taskfile.dev/installation).

choco install go-task -y

Then, we can create a file like this called Taskfile.yml.

version: '3'

tasks:
  build-and-run:
    - task: build-image
    - task: create-vm

  build-image:
    cmds:
      - powershell -command 'if (Test-Path 'output-windows') { Remove-Item -Path 'output-windows' -Recurse -Force }'
      - powershell -command 'if (Test-Path "$env:USERPROFILE\VirtualBox VMs\win2022") { Remove-Item -Path "$env:USERPROFILE\VirtualBox VMs\win2022" -Recurse -Force }'
      - packer build ./windows.pkr.hcl
      - qemu-img convert -f vmdk -O qcow2  ./output-windows/win2022-disk001.vmdk D:/QCOW/windows-2022.qcow2

  create-vm:
    cmds:
      - kubectl apply  -f kubevirt_win2022_pvc.yml
      - kubectl apply  -f kubevirt_win2022_dv.yml
      - kubectl apply  -f kubevirt_win2022_vm.yml

  status-vm:
    cmds:
      - kubectl describe vm win2022-vm

  vnc-vm:
    cmds:
      - virtctl vnc win2022-vm

  stop-vm:
    cmds:
      - virtctl stop win2022-vm

  start-vm:
    cmds:
      - virtctl start win2022-vm

  delete-vm:
    cmds:
      - kubectl delete  -f kubevirt_win2022_pvc.yml
      - kubectl delete  -f kubevirt_win2022_dv.yml
      - kubectl delete  -f kubevirt_win2022_vm.yml

Now, to build an image and start a VM, we can run this

task build-and-run

And to delete it, we can run this

task delete-vm

There are a couple more, to see what is available (easy to forget) simply run task --list-all

task: Available tasks for this project:
* build-and-run:
* build-image:
* config-kind-cluster:
* create-and-config-kind-cluster:
* create-kind-cluster:
* create-svc:
* create-vm:
* delete-kind-cluster:
* delete-vm:
* install-crds-kind-cluster:
* nodeips-kind-cluster:
* start-vm:
* status-vm:
* stop-vm:
* vnc-vm:

A very nice simplification, and something I am going to use more of in the future.

Here is a screenshot of creating a VM so you can see how it works.

Speed Run with Taskfile

If you have Packer, VirtualBox, Virtctl, and Kubectl installed, you can do more of a speedrun to recreate a cluster from scratch with the help of Taskfile. You can start from zero to a full cluster like this.

task delete-kind-cluster
task create-and-config-kind-cluster  # Wait a minute for things to install. If anyone knows a simple command to wait for everything to go ready, let me know!
task build-and-run-vm
task vnc-vm

Amazing!

If you made it this far I salute you. It was a lot to do, but hopefully, someone found it helpful!

Here are some other blogs I found which might be helpful as well