A Working Kubevirt Tutorial
A Working Kubevirt Tutorial 🙂
Why?
Well, after installing a few pre-packaged Kubernetes Virtualization platforms, I decided to try adding KubeVirt manually to Kubernetes. The KubeVirt tutorials out there mostly don't work, unfortunately. I finally found a process that works and thought I would write the process down to help anyone else that may be struggling. (Also, I recently found this tutorial which was more helpful/accurate than most.)
What?
You will need:
- Access to a computer or virtual machine or cloud VM
- A Linux distribution
- A Kubernetes distribution (I have been successful with quite a few. Microk8s, Minikube, k3s, Kind are super easy to install and have low overhead.)
- Patience
- Possibly, a quiet room to scream in
How?
Install Kubernetes
Any Kubernetes should work. For this blog, we'll install the SUSE Rancher k3s Kubernetes. For no particular reason; it's just easy to install and it works. What's also fun is the k3OS ISO that you can boot and have a working Linux distro with Kubernetes and kubectl pre-installed. k3OS is an ephemeral OS, so keep that in mind.
To install k3s:
$ curl -sfL https://get.k3s.io | sh -
[INFO] Finding release for channel stable
[INFO] Using v1.29.6+k3s2 as release
[INFO] Downloading hash https://github.com/k3s-io/k3s/releases/download/v1.29.6+k3s2/sha256sum-amd64.txt
[INFO] Downloading binary https://github.com/k3s-io/k3s/releases/download/v1.29.6+k3s2/k3s
[INFO] Verifying binary download
[INFO] Installing k3s to /usr/local/bin/k3s
[INFO] Skipping installation of SELinux RPM
[INFO] Skipping /usr/local/bin/kubectl symlink to k3s, command exists in PATH at /snap/bin/kubectl
[INFO] Creating /usr/local/bin/crictl symlink to k3s
[INFO] Skipping /usr/local/bin/ctr symlink to k3s, command exists in PATH at /usr/bin/ctr
[INFO] Creating killall script /usr/local/bin/k3s-killall.sh
[INFO] Creating uninstall script /usr/local/bin/k3s-uninstall.sh
[INFO] env: Creating environment file /etc/systemd/system/k3s.service.env
[INFO] systemd: Creating service file /etc/systemd/system/k3s.service
[INFO] systemd: Enabling k3s unit
Created symlink /etc/systemd/system/multi-user.target.wants/k3s.service → /etc/systemd/system/k3s.service.
[INFO] systemd: Starting k3s
$ sudo k3s kubectl get node
NAME STATUS ROLES AGE VERSION
hostname Ready control-plane,master 33s v1.29.6+k3s2
To interact with k3s, you can either alias kubectl='sudo k3s kubectl' or install the kubectl binary from here.
Here are all the Kubernetes components running with a fresh install of k3s:
$ kubectl get all -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system pod/coredns-6799fbcd5-cjmzs 1/1 Running 0 5m19s
kube-system pod/helm-install-traefik-46pgd 0/1 Completed 1 5m19s
kube-system pod/helm-install-traefik-crd-jgfdm 0/1 Completed 0 5m19s
kube-system pod/local-path-provisioner-6f5d79df6-v4fnl 1/1 Running 0 5m19s
kube-system pod/metrics-server-54fd9b65b-rdglt 1/1 Running 0 5m19s
kube-system pod/svclb-traefik-d52c11d5-hq6wb 2/2 Running 0 5m12s
kube-system pod/traefik-7d5f6474df-h8zw2 1/1 Running 0 5m12s
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
default service/kubernetes ClusterIP 10.43.0.1 <none> 443/TCP
kube-system service/kube-dns ClusterIP 10.43.0.10 <none> 53/UDP,53/TCP,9153/TCP
kube-system service/metrics-server ClusterIP 10.43.4.117 <none> 443/TCP
kube-system service/traefik LoadBalancer 10.43.152.158 192.168.1.188 80:30948/TCP, 443:32562/TCP
NAMESPACE NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE
kube-system daemonset.apps/svclb-traefik-d52c11d5 1 1 1 1 1
NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE
kube-system deployment.apps/coredns 1/1 1 1 5m30s
kube-system deployment.apps/local-path-provisioner 1/1 1 1 5m30s
kube-system deployment.apps/metrics-server 1/1 1 1 5m30s
kube-system deployment.apps/traefik 1/1 1 1 5m12s
NAMESPACE NAME DESIRED CURRENT READY AGE
kube-system replicaset.apps/coredns-6799fbcd5 1 1 1 5m19s
kube-system replicaset.apps/local-path-provisioner-6f5d79df6 1 1 1 5m19s
kube-system replicaset.apps/metrics-server-54fd9b65b 1 1 1 5m19s
kube-system replicaset.apps/traefik-7d5f6474df 1 1 1 5m12s
NAMESPACE NAME COMPLETIONS DURATION AGE
kube-system job.batch/helm-install-traefik 1/1 21s 5m28s
kube-system job.batch/helm-install-traefik-crd 1/1 11s 5m28s
Install the KubeVirt base components
$ KUBEVIRT_VERSION=$(curl -s https://api.github.com/repos/kubevirt/kubevirt/releases/latest | awk -F '[ \t":]+' '/tag_name/ {print $3}')
From kubebyexample: "This command will create a namespace, "kubevirt" and install the Custom Resource Definitions for KubeVirt we described earlier, as well as an operator that will wait for a configuration resource to launch the KubeVirt installation."
$ kubectl create -f https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/kubevirt-operator.yaml
namespace/kubevirt created
customresourcedefinition.apiextensions.k8s.io/kubevirts.kubevirt.io created
priorityclass.scheduling.k8s.io/kubevirt-cluster-critical created
clusterrole.rbac.authorization.k8s.io/kubevirt.io:operator created
serviceaccount/kubevirt-operator created
role.rbac.authorization.k8s.io/kubevirt-operator created
rolebinding.rbac.authorization.k8s.io/kubevirt-operator-rolebinding created
clusterrole.rbac.authorization.k8s.io/kubevirt-operator created
clusterrolebinding.rbac.authorization.k8s.io/kubevirt-operator created
deployment.apps/virt-operator created
From kubebyexample: "Now we add the Kubevirt Custom Resource (called kubevirt) that will trigger the KubeVirt operator to install the rest of KubeVirt:"
$ kubectl create -f https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/kubevirt-cr.yaml
kubevirt.kubevirt.io/kubevirt created
Let's see what the new kubevirt namespace contains now:
$ kubectl get all -n kubevirt
NAME READY STATUS RESTARTS AGE
pod/virt-api-98cf864cc-vjqsf 1/1 Running 0 4m14s
pod/virt-controller-5d65ddf8c5-drzp5 1/1 Running 0 3m49s
pod/virt-controller-5d65ddf8c5-j4xms 1/1 Running 0 3m49s
pod/virt-handler-9lj6m 1/1 Running 0 3m49s
pod/virt-operator-865f487cf6-4crdd 1/1 Running 0 5m30s
pod/virt-operator-865f487cf6-5ghh9 1/1 Running 0 5m30s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
service/kubevirt-operator-webhook ClusterIP 10.43.100.27 <none> 443/TCP
service/kubevirt-prometheus-metrics ClusterIP None <none> 443/TCP
service/virt-api ClusterIP 10.43.207.89 <none> 443/TCP
service/virt-exportproxy ClusterIP 10.43.7.96 <none> 443/TCP
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
daemonset.apps/virt-handler 1 1 1 1 1 kubernetes.io/os=linux 3m49s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/virt-api 1/1 1 1 4m14s
deployment.apps/virt-controller 2/2 2 2 3m49s
deployment.apps/virt-operator 2/2 2 2 5m30s
NAME DESIRED CURRENT READY AGE
replicaset.apps/virt-api-98cf864cc 1 1 1 4m14s
replicaset.apps/virt-controller-5d65ddf8c5 2 2 2 3m49sreplicaset.apps/virt-operator-865f487cf6 2 2 2 5m30sNAME AGE PHASEkubevirt.kubevirt.io/kubevirt 4m36s Deployed
Install the virtctl binary for starting, stopping, and using the console of VMs
$ curl -Lo virtctl https://github.com/kubevirt/kubevirt/releases/download/${KUBEVIRT_VERSION}/virtctl-${KUBEVIRT_VERSION}-linux-amd64
$ chmod +x virtctl
$ sudo cp virtctl /usr/local/bin
$ export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
$ sudo chmod +r /etc/rancher/k3s/k3s.yaml
So, something I noticed is that if the version of the kubevirt client is not exactly the same as the kubevirt server, it acts really bizarre. You may have to force a download of the matching version.
$ virtctl version
Client Version: version.Info{GitVersion:"v1.3.0-rc.1", GitCommit:"d1cf0d0d2419ea582ae172509431aaf2276b7344", GitTreeState:"clean", BuildDate:"2024-06-26T14:54:43Z", GoVersion:"go1.22.2 X:nocoverageredesign", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{GitVersion:"v1.2.2", GitCommit:"1466b658f78b9b8bb9517ffb6dafd4b777f33fe6", GitTreeState:"clean", BuildDate:"2024-06-06T05:46:26Z", GoVersion:"go1.21.8 X:nocoverageredesign", Compiler:"gc", Platform:"linux/amd64"}
To fix this
$ curl -Lo virtctl https://github.com/kubevirt/kubevirt/releases/download/v1.2.2/virtctl-v1.2.2-linux-amd64
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 43.2M 100 43.2M 0 0 22.9M 0 0:00:01 0:00:01 --:--:-- 34.6M
$ chmod +x virtctl
$ ./virtctl version
Client Version: version.Info{GitVersion:"v1.2.2", GitCommit:"1466b658f78b9b8bb9517ffb6dafd4b777f33fe6", GitTreeState:"clean", BuildDate:"2024-06-06T04:42:11Z", GoVersion:"go1.21.8 X:nocoverageredesign", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{GitVersion:"v1.2.2", GitCommit:"1466b658f78b9b8bb9517ffb6dafd4b777f33fe6", GitTreeState:"clean", BuildDate:"2024-06-06T05:46:26Z", GoVersion:"go1.21.8 X:nocoverageredesign", Compiler:"gc", Platform:"linux/amd64"}
$ sudo cp virtctl /usr/local/bin
Make sure you have hardware virtualization turned on in Linux
(If you do not, your VM will fail to start with error "ErrorUnschedulable")
$ sudo apt install cpu-checker
$ sudo kvm-ok
INFO: /dev/kvm exists
KVM acceleration can be used
Perform a "smoke test"
This YAML is used by kubectl to create a very small, simple, 30MB disk, Linux VM
$ kubectl apply -f https://kubevirt.io/labs/manifests/vm.yaml
virtualmachine.kubevirt.io/testvm created
$ kubectl get vm
NAME AGE STATUS READY
testvm 50s Stopped False
$ virtctl start testvm
VM testvm was scheduled to start
$ kubectl get vm,vmi,pod
NAME AGE STATUS READY
virtualmachine.kubevirt.io/testvm 2m37s Running True
NAME AGE PHASE IP NODENAME READY
virtualmachineinstance.kubevirt.io/testvm 23s Running 10.42.0.28 xubuntu-vm True
NAME READY STATUS RESTARTS AGE
pod/virt-launcher-testvm-pjqqz 3/3 Running 0 23s
$ virtctl console testvm
Successfully connected to testvm console. The escape sequence is ^]
login as 'cirros' user. default password: 'gocubsgo'. use 'sudo' for root.
testvm login: cirros
Password:$ free -m total used free shared buffersMem: 43 35 7 0 1-/+ buffers: 34 8Swap: 0 0 0$ uname -aLinux testvm 4.4.0-28-generic #47-Ubuntu SMP Fri Jun 24 10:09:13 UTC 2016 x86_64 GNU/Linux$ df -hFilesystem Size Used Available Use% Mounted on/dev/vda1 29.7M 23.0M 5.0M 82% /
OK, the simple "smoke test" Linux VM is running, and we can login to the console. To exit the console, just press CTRL-]
Install a Full, Custom, Debian VM
The "smoke test" VM was a tiny, bare bones, VM. For a full VM, it would be nice to choose the disk size and the Linux distribution.
In order to define kubevirt VM disks as Kubernetes Persistent Volumes, we need to install the kubevirt Containerized Data Importer (CDI). Kubebyexample has some node instructions on installing CDI here.
$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path (default) rancher.io/local-path Delete WaitForFirstConsumer false 3d1h
$ export VERSION=$(curl -Ls https://github.com/kubevirt/containerized-data-importer/releases/latest | grep -m 1 -o "v[0-9]\.[0-9]*\.[0-9]*")
$ echo $VERSION
v1.59.0
$ kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/$VERSION/cdi-operator.yaml
namespace/cdi created
customresourcedefinition.apiextensions.k8s.io/cdis.cdi.kubevirt.io created
clusterrole.rbac.authorization.k8s.io/cdi-operator-cluster created
clusterrolebinding.rbac.authorization.k8s.io/cdi-operator created
serviceaccount/cdi-operator created
role.rbac.authorization.k8s.io/cdi-operator created
rolebinding.rbac.authorization.k8s.io/cdi-operator created
deployment.apps/cdi-operator created
$ kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/$VERSION/cdi-cr.yaml
cdi.cdi.kubevirt.io/cdi created
$ kubectl get cdi -n cdi
NAME AGE PHASE
cdi 61s Deployed
OK. Now let's create a 64GB "Data Volume" that will be the boot disk for our Debian VM.
dennis@xubuntu-vm:~$ kubectl create -f - <<EOF
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
name: "debian"
spec:
source:
http:
url: "https://cloud.debian.org/images/cloud/bullseye/latest/debian-11-generic-amd64.qcow2"
pvc:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: "64Gi"
EOF
datavolume.cdi.kubevirt.io/debian created
$ kubectl get dv,pvc,vm,vmi
NAME PHASE PROGRESS RESTARTS AGE
datavolume.cdi.kubevirt.io/debian WaitForFirstConsumer N/A 45m
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/debian Pending local-path <unset> 45m
NAME AGE STATUS READY
virtualmachine.kubevirt.io/testvm 3d1h Running True
NAME AGE PHASE IP NODENAME READY
virtualmachineinstance.kubevirt.io/testvm 3d1h Running 10.42.0.28 xubuntu-vm True
You can find other debian qcow2 images for use with QEMU or kubevirt (which is using QEMU) at this site. You can find Ubuntu images here and Fedora images here.
Well, that 64GB Debian Data Volume is waiting for a VM to use it, so let's use it.
$ kubectl create -f - <<EOF
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
labels:
kubevirt.io/os: linux
name: debian
spec:
running: true
template:
metadata:
creationTimestamp: null
labels:
kubevirt.io/domain: debian
spec:
domain:
cpu:
cores: 1
devices:
disks:
- disk:
bus: virtio
name: disk0
- cdrom:
bus: sata
readonly: true
name: cloudinitdisk
resources:
requests:
memory: 2G
volumes:
- name: disk0
persistentVolumeClaim:
claimName: debian
- cloudInitNoCloud:
userData: |
#cloud-config
system_info:
default_user:
name: dennis
home: /home/dennis
password: SuperSecretPassword!
chpasswd: { expire: False }
hostname: debian-k8s
ssh_pwauth: True
disable_root: false
ssh_authorized_keys:
- ssh-rsa YOUR_SSH_PUB_KEY_HERE
name: cloudinitdisk
EOF
virtualmachine.kubevirt.io/debian created
Notice the sections where you can define the number of CPU cores, the amount of RAM, a user account, that user's initial password, and the hostname. Also notice where we specified the Persistent Volume Claim name that we created in the last step.
Let's check on our VM
$ kubectl get dv,pvc,vm,vmi
NAME PHASE PROGRESS RESTARTS AGE
datavolume.cdi.kubevirt.io/debian Succeeded 100.0% 56m
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/debian Bound pvc-b9cb1c01-eed2-478b-994a-933bc1a6287c 64Gi RWO local-path <unset> 56m
NAME AGE STATUS READY
virtualmachine.kubevirt.io/debian 8m8s Running True
virtualmachine.kubevirt.io/testvm 3d1h Running True
NAME AGE PHASE IP NODENAME READY
virtualmachineinstance.kubevirt.io/debian 8m8s Running 10.42.0.38 xubuntu-vm True
virtualmachineinstance.kubevirt.io/testvm 3d1h Running 10.42.0.28 xubuntu-vm True
$ virtctl console debian
Successfully connected to debian console. The escape sequence is ^]
debian-k8s login: dennis
Password: [SuperSecretPassword!]
Linux debian-k8s 5.10.0-30-amd64 #1 SMP Debian 5.10.218-1 (2024-06-01) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
dennis@debian-k8s:~$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 60G 873M 57G 2% /
virtctl Options
While running virtctl --help, I noticed some interesting options
$ virtctl --help
virtctl controls virtual machine related operations on your kubernetes cluster.
Available Commands:
addvolume add a volume to a running VM
completion Generate the autocompletion script for the specified shell
console Connect to a console of a virtual machine instance.
create Create a manifest for the specified Kind.
credentials Manipulate credentials on a virtual machine.
expand Return the VirtualMachine object with expanded instancetype and preference.
expose Expose a virtual machine instance, virtual machine, or virtual machine instance replica set as a new service.
fslist Return full list of filesystems available on the guest machine.
guestfs Start a shell into the libguestfs pod
guestosinfo Return guest agent info about operating system.
help Help about any command
image-upload Upload a VM image to a DataVolume/PersistentVolumeClaim.
memory-dump Dump the memory of a running VM to a pvc
migrate Migrate a virtual machine.
migrate-cancel Cancel migration of a virtual machine.
pause Pause a virtual machine
permitted-devices List the permitted devices for vmis.
port-forward Forward local ports to a virtualmachine or virtualmachineinstance.
removevolume remove a volume from a running VM
restart Restart a virtual machine.
scp SCP files from/to a virtual machine instance.
soft-reboot Soft reboot a virtual machine instance
ssh Open a SSH connection to a virtual machine instance.
start Start a virtual machine.
stop Stop a virtual machine.
unpause Unpause a virtual machine
usbredir Redirect a usb device to a virtual machine instance.
userlist Return full list of logged in users on the guest machine.
version Print the client and server version information.
vmexport Export a VM volume.
vnc Open a vnc connection to a virtual machine instance.
Let's take a few of these options in turn
console: We've already covered this one
guestinfo: If you have installed the qemu-guest-agent package in your Linux vm, this command provides everything you might need to know about the running Linux OS
virtctl guestosinfo debian
{
[SNIP]
"guestAgentVersion": "5.2.0",
"supportedCommands": [
"hostname": "debian-k8s",
"os": {
"name": "Debian GNU/Linux",
"kernelRelease": "5.10.0-30-amd64",
"version": "11 (bullseye)",
"prettyName": "Debian GNU/Linux 11 (bullseye)",
"versionId": "11",
"kernelVersion": "#1 SMP Debian 5.10.218-1 (2024-06-01)",
"machine": "x86_64",
"id": "debian"
},
"timezone": "UTC, 0",
"userList": [
{
"userName": "dennis",
"loginTime": 1720663137.621031
}
],
"fsInfo": {
"disks": [
{
"diskName": "vda15",
"mountPoint": "/boot/efi",
"fileSystemType": "vfat",
"usedBytes": 11161600,
"totalBytes": 129718272
},
{
"diskName": "vda1",
"mountPoint": "/",
"fileSystemType": "ext4",
"usedBytes": 1123741696,
"totalBytes": 27422138368
}
]
},
"fsFreezeStatus": "thawed"
}
ssh: set up passwordless login to the VM
If you create the VM with access to your laptops public key, you can then use passwordless ssh to access the VM. These are the steps:
$ kubectl create secret generic my-pub-key --from-file=key1=/Users/faucherd/.ssh/id_rsa.pub
secret/my-pub-key created
$ vi debian-vm.yaml
[SNIP]
accessCredentials:
- sshPublicKey:
source:
secret:
secretName: my-pub-key
propagationMethod:
qemuGuestAgent:
users:
- dennis
volumes:
$ kubectl create -f debian-vm.yaml
virtualmachine.kubevirt.io/debian created
$ virtctl ssh --local-ssh dennis@debian
Linux debian-k8s 5.10.0-30-amd64 #1 SMP Debian 5.10.218-1 (2024-06-01) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Thu Jul 11 19:04:46 2024 from 10.42.0.51
dennis@debian-k8s:~$
vnc: Open up a graphical VNC connection to the VM
This option requires the remote-viewer app to be installed on the client machine or to proxy VNC connections from your client machine with "virtctl vnc --proxy-only testvm". the remote-viewer app can be installed for different operating systems using the instructions on this page. I use a Mac client, so these are my instructions:
$ brew tap jeffreywildman/homebrew-virt-manager
$ brew install virt-manager virt-viewer
$ virtctl vnc debian
Exciting. Opened up TigerVNC automagically on my Mac
Connecting via non-proxied VNC on a Linux client was pretty straightforward:
Here are the instruction to proxy the VNC port on a Mac client and use a native Mac VNC app (TigerVNC):
$ virtctl vnc --proxy-only debian
{"port":54953}
{"component":"","level":"info","msg":"connection timeout: 1m0s","pos":"vnc.go:157","timestamp":"2024-07-11T15:46:19.091701Z"}
{"component":"","level":"info","msg":"VNC Client connected in 9.943800333s","pos":"vnc.go:170","timestamp":"2024-07-11T15:46:29.035611Z"}
..and it worked. Amazing.
Here is the same working proxy connection on a Linux client
Success
Thank You
Well, there you have it. Thanks for following along. I hope this saves you some time or at least provides some educational value. I welcome your feedback