How to Run a Windows VM on Kubernetes with KubeVirt: Step‑by‑Step Guide
This tutorial explains how to leverage KubeVirt on a Kubernetes cluster to create and run a Windows 10 virtual machine, covering architecture, disk options, preparation, installation, image upload, VM definition, networking, VNC/RDP access, and CNI troubleshooting in a comprehensive, hands‑on manner.
Recently I discovered that my Kubernetes cluster has many idle resources, so I decided to use them to run a Windows virtual machine. Since I only have a MacBook without Windows, I chose KubeVirt, a Red Hat open‑source project that runs VMs as containers via CRDs.
1. KubeVirt Architecture Design
KubeVirt implements several resources to manage virtual machines: VirtualMachineInstance (VMI): the smallest VM resource, similar to a Pod, representing a running VM instance. VirtualMachine (VM): provides management functions (start/stop/reboot) for a VMI, analogous to a StatefulSet with spec.replicas set to 1. VirtualMachineInstanceReplicaSet: like a ReplicaSet, ensures a specified number of VMIs are running and can be scaled with HPA.
The overall KubeVirt architecture includes:
virt-api : exposes KubeVirt‑specific APIs such as console, VNC, startvm, stopvm.
virt-controller : monitors and updates VMI objects and their associated Pods.
virt-handler : runs as a DaemonSet on each node, reports VMI status and manages the VM lifecycle.
virt-launcher : runs as a Pod for each VMI, containing libvirtd to start and manage the VM.
2. Disks and Volumes
KubeVirt supports various disk types for VM images:
PersistentVolumeClaim : uses a PVC for persistent storage (block or filesystem). In filesystem mode, /disk.img (RAW format) is used; in block mode, the block device is attached directly.
ephemeral : creates a copy‑on‑write layer on local storage; the layer is removed when the VM stops.
containerDisk : a Docker image that contains a VM image; it can be pulled from a registry but does not support persistence.
hostDisk : uses a host‑node disk image, similar to hostPath, optionally creating an empty image.
dataVolume : CDI feature that imports a VM image into a PVC from HTTP, S3, another PVC, or a container registry.
3. Preparation
Install libvirt and QEMU packages first:
# Ubuntu
apt install -y qemu-kvm libvirt-bin bridge-utils virt-manager
# CentOS
yum install -y qemu-kvm libvirt virt-install bridge-utilsVerify KVM hardware support:
$ virt-host-validate qemu
# All checks PASSIf hardware acceleration is unavailable, enable software emulation:
$ kubectl create namespace kubevirt
$ kubectl create configmap -n kubevirt kubevirt-config \
--from-literal debug.useEmulation=true4. Install KubeVirt
Deploy the latest version
$ export VERSION=$(curl -s https://api.github.com/repos/kubevirt/kubevirt/releases \
| grep tag_name | grep -v -- '-rc' | head -1 | awk -F': ' '{print $2}' | sed 's/,//' | xargs)
$ kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/kubevirt-operator.yaml
$ kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/kubevirt-cr.yamlCheck the pods in the kubevirt namespace to ensure they are running.
Deploy CDI (Containerized Data Importer)
$ export VERSION=$(curl -s https://github.com/kubevirt/containerized-data-importer/releases/latest \
| grep -o "v[0-9]\.[0-9]*\.[0-9]*")
$ kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/${VERSION}/cdi-operator.yaml
$ kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/${VERSION}/cdi-cr.yaml5. Client Tools
Download the virtctl CLI:
$ export VERSION=$(curl -s https://api.github.com/repos/kubevirt/kubevirt/releases \
| grep tag_name | grep -v -- '-rc' | head -1 | awk -F': ' '{print $2}' | sed 's/,//' | xargs)
$ curl -L -o /usr/local/bin/virtctl https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/virtctl-${VERSION}-linux-amd64
$ chmod +x /usr/local/bin/virtctlAlternatively install it as a kubectl plugin via krew:
$ kubectl krew install virt6. Prepare the Windows Image
Download a Windows ISO from a trusted source (e.g., MSDN I Tell You or TechBench by WZT) and upload it to a PVC using CDI:
$ virtctl image-upload \
--image-path='Win10_20H2_Chinese(Simplified)_x64.iso' \
--pvc-name=iso-win10 \
--pvc-size=7G \
--uploadproxy-url=https://<cdi-uploadproxy_svc_ip> \
--insecure \
--wait-secs=240Key parameters:
--image-path : path to the OS image.
--pvc-name : name of the PVC that will be created automatically.
--pvc-size : size of the PVC (slightly larger than the image).
--uploadproxy-url : service IP of the CDI upload proxy.
7. Create the VM
Define a VM manifest ( win10.yaml) that uses three volumes:
cdromiso : the PVC containing the Windows ISO.
harddrive : a hostDisk on the node for the OS installation.
virtiocontainerdisk : a container disk providing virtio drivers.
apiVersion: kubevirt.io/v1alpha3
kind: VirtualMachine
metadata:
name: win10
spec:
running: false
template:
metadata:
labels:
kubevirt.io/domain: win10
spec:
domain:
cpu:
cores: 4
devices:
disks:
- bootOrder: 1
cdrom:
bus: sata
name: cdromiso
- disk:
bus: virtio
name: harddrive
- cdrom:
bus: sata
name: virtiocontainerdisk
interfaces:
- masquerade: {}
model: e1000
name: default
machine:
type: q35
resources:
requests:
memory: 16G
networks:
- name: default
pod: {}
volumes:
- name: cdromiso
persistentVolumeClaim:
claimName: iso-win10
- name: harddrive
hostDisk:
capacity: 50Gi
path: /data/disk.img
type: DiskOrCreate
- name: virtiocontainerdisk
containerDisk:
image: kubevirt/virtio-container-diskCreate and start the VM:
$ kubectl apply -f win10.yaml
$ virtctl start win10 # or: kubectl virt start win10Verify the VM pod is running:
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
virt-launcher-win10-... 2/2 Running 0 15s8. Access the VM
Use VNC for initial access (install a VNC viewer such as RealVNC on macOS):
$ virtctl vnc win10 # or: kubectl virt vnc win10After Windows installation, install the virtio drivers from the attached containerDisk to enable proper disk detection.
9. CNI Plugin Issue (Calico)
If using Calico, enable IP forwarding for the masquerade mode:
$ kubectl -n kube-system edit cm calico-config
# Add:
"container_settings": {"allow_ip_forwarding": true}Restart the Calico node pods:
$ kubectl -n kube-system delete pod -l k8s-app=calico-node10. Remote Desktop (RDP)
Enable RDP inside Windows, then expose the RDP port via a NodePort service:
$ kubectl virt expose vm win10 --name win10-rdp --port 3389 --target-port 3389 --type NodePort
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
win10-rdp NodePort 10.98.20.203 <none> 3389:31192/TCP 20mConnect using the node IP and the allocated node port (e.g., NodeIP:31192) from any RDP client (Windows Remote Desktop, Microsoft Remote Desktop on macOS, etc.).
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
