How to run a virtual machine on Kubernetes using KubeVirt

The KubeVirt project logo.
BlogLink to blog
Blog
7 min read

My experience with using KubeVirt to run a virtual machine on a bare metal Kubernetes server. Image is of the KubeVirt project logo.


Contents


Introduction 

I’m a big fan of Kubernetes 🔗, but virtual machines still have their place. Sometimes you just need a full-on virtualized PC and not a container. At the same time, I’ve gotten used to running all of my services on Kubernetes, so I got to thinking: can I run a virtual machine inside Kubernetes? As it turns out (unsurprisingly), I’m not the first person to think this.

Here’s how I learned about KubeVirt, and spun up a virtual machine on my Kubernetes server.

A quick intro to containers and Kubernetes 

For the uninitiated and non-nerdy, a container is a method of running applications as portable, self-contained units. Think of it like sending a letter to someone—instead of just dropping a letter in the postbox, you’d use an envelope as a container to wrap it in before sending it out. But when you have a bunch of letters to send at once, you want some kind of system to handle creating envelopes, wrapping the letters, and tracking them. That’s what Kubernetes is to containers.

Imagine you wanted to host a website, like this blog. You could get a regular computer, run a bunch of commands to install a web server on it, copy the raw files over, then configure the web server to serve those files. That’s the traditional way of doing things, and it’s all well and good for a single website, but what if you wanted to run a dozen websites like this? A hundred? A thousand? Would you want to run through this process each time? Probably not. At any rate, would you want to monitor all of these sites in case one went offline? Sounds like a lot of work.

Instead, you could take your website files and package them into a container. The container includes everything needed to run the site, including the web server, configuration, and files. You can define this container using what’s called a manifest, then use Kubernetes to deploy it. You can reuse this container for different sites by just copying in a different set of files. Kubernetes will take each container, figure out where and how to deploy them, and do all the rest, including monitoring them to see if they go offline.

However, Kubernetes is a complex system. It introduces tons of unique concepts and terminology, has a labyrinthine set of interactions between resources, takes some in-depth system administration knowledge to set up, and has some esoteric error messages (like the dreaded CrashLoopBackOff). The good news is there are projects that make Kubernetes relatively easy to install, including the one I’m using: K3s 🔗.

K3s is a lightweight distribution of Kubernetes that makes installing and managing a Kubernetes cluster relatively painless. All you need to do is run a script on the computers you want to run K3s on. If you want to create a cluster by grouping multiple computers into the same Kubernetes cluster, you just run the script with the IP address of the primary/controlling computer.

A quick intro to virtual machines 

Before containers became vogue, virtual machines (VMs) were the Big Thing™️. Virtual machines are like the big cousins of containers—they also let you run applications and services in a self-contained package, but the package is an entire computer. Imagine taking your laptop and installing a completely separate copy of Windows (or MacOS or Linux or whatever you use) that you could run without having to reboot. You could have a dozen different copies running completely different applications simultaneously, and as long as you had powerful enough hardware, the copies would have no idea they were virtual.

The great thing about VMs are that they’re fully isolated. I don’t have to worry as much about breaking my Arch Linux installation if I do something stupid in a VM, like accidentally downloading a virus or running a dangerous command. The downside is that VMs generally use far more resources than containers to run the same software. This is because they need to emulate an entire computer, including the operating system. It’s like trying to simulate a second, separate mind within your own mind.

Bridging containers and virtual machines with KubeVirt 

This brings us to KubeVirt 🔗. To put it simply, KubeVirt lets you run virtual machines on Kubernetes as if they were containers.

Why would you want to do that? Well, imagine you have applications that require isolation like in a VM, or that you can’t easily package into a container. Instead of having to manage two separate systems (Kubernetes and a VM hypervisor), you can do it all through Kubernetes. KubeVirt also provides tools for connecting to VMs using a GUI or console.

It’s probably better if I give an example. I have a VM that I use to test software for work. This software is a type of QA software that changes things about the system, like consuming excess CPU and RAM, dropping network connections, and adding latency. Running it directly on my server could interfere with the other services running on the server, or just outright crash the server itself. Putting it on a VM gives me a layer of isolation that isn’t as straightforward to get with a container—plus, if something goes wrong, I can always terminate the VM and delete it, or restore from a snapshot.

Deploying KubeVirt 

Right, so, what do you need to run KubeVirt?

  1. A Kubernetes (in my case, K3s) cluster.
  2. The kubectl command-line tool.

The first step is to deploy the components that let you run KubeVirt. This is pretty easy, just follow the quickstart instructions 🔗. You’ll also want to install virtctl 🔗, which is a wrapper for kubectl that makes managing and interacting with VMs easier. KubeVirt lets you interact with your VMs by connecting directly to serial console, using SSH, or by using VNC 🔗 to access a graphical interface. If you want to use VNC, you’ll also need to install remote-viewer 🔗.

Unlike virtualization tools like Gnome Boxes or VirtualBox, you can’t connect directly to a KubeVirt VM. You have to use VNC. That said, virtctl works everywhere kubectl does, so it’s very easy to set up and use.

Defining a virtual machine 

You can define a virtual machine instance the same way you’d define a Kubernetes manifest for a container.

First, though, if you’re installing a VM from scratch using an ISO image, you’ll need to enable a feature gate to allow KubeVirt to access the ISO image on the host. I created a YAML file called kubevirt-feature-gate.yaml for that:

apiVersion: kubevirt.io/v1
kind: KubeVirt
metadata:
  name: kubevirt
  namespace: kubevirt
spec:
  configuration:
    developerConfiguration:
      featureGates:
        - HostDisk

Now, let’s define our new VM. Here, I’m creating a VM called work-lab and giving it 2 CPU cores and 4 GB of RAM. I’m also attaching a Debian ISO image as the boot image, plus a persistent volume named work-lab-hd that will act as the VM’s hard drive. The VM sees this like a regular drive so you can install to it, but on the backend, it uses normal Kubernetes PersistentVolumes, so you can have it store pretty much anywhere 🔗. Isn’t Kubernetes great?

This volume appears like a regular drive to the VM, so you can install to it and save files to it. On the backend, you can store this drive wherever and however you’d like, including remotely using NFS or S3, and the VM won’t know the difference. Here, I’m storing both the ISO and VM hard drive on a folder on my server called /storage/services/kubevirt/:

# Define storage for the VM
apiVersion: v1
kind: PersistentVolume
metadata:
  name: work-lab-hd
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 60Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/storage/services/kubevirt/work-lab"
---
 apiVersion: v1
 kind: PersistentVolumeClaim
 metadata:
   name: work-lab-hd
 spec:
   accessModes:
     - ReadWriteOnce
   resources:
     requests:
       storage: 60Gi
   storageClassName: manual
---
# Define the VM itself
 apiVersion: kubevirt.io/v1alpha3
 kind: VirtualMachine
 metadata:
   name: work-lab
 spec:
   running: false
   template:
     metadata:
       labels:
         kubevirt.io/domain: work-lab
     spec:
       domain:
         cpu:
           cores: 2
         devices:
           disks:
           - cdrom:
               bus: sata
             name: cdromiso
             bootOrder: 1
           - disk:
               bus: virtio
             name: harddrive
             bootOrder: 2
         machine:
           type: q35
         resources:
           requests:
             memory: 4G
       volumes:
       - name: cdromiso
         hostDisk:
           path: "/storage/Software/ISOs/debian-12.1.0-amd64-netinst.iso"
           type: Disk
       - name: harddrive
         persistentVolumeClaim:
           claimName: work-lab-hd
---

To run this, just save it as a YAML file and apply it using kubectl:

kubectl apply -f work-vm.yaml

Now, we just wait for the instance to come up:

kubectl wait --for=condition=Ready vmis/work-vm

Once it’s up, you can open a graphical interface to the instance using virtctl 🔗:

virtctl vnc work-vm

And there you go! You can now install and use the VM just like if you were using VirtualBox, KVM, or any other VM management tool!

One thing though: after you install the OS and shut down the system, either change the boot order for the ISO image or delete that part of the manifest. Otherwise, the VM will boot into the ISO each time you start it up.

Networking, additional features, etc. 

This blog is already getting super long, so I’ll make this section quick. Your VM instance now behaves like any other Kubernetes resource. You can use Services and Ingresses to manage connectivity, you can scale it up by creating replicas, you can set resource limits, and much more. These are all way beyond the scope of this blog, so check out the KubeVirt docs 🔗 to see what features it supports and how to enable and configure them.

Conclusion 

I’m all for reducing the number of different tools I need to use to manage my systems, and KubeVirt finally let me consolidate my VMs into Kubernetes. It only took me a few hours to learn KubeVirt and recreate my work VM in it, so it’s very accessible if you’re already a Kubernetes user. Check it out at kubevirt.io 🔗!

Previous: "3 weeks with Obsidian"Next: "Reflecting on Everything Everywhere All at Once"
art atmospheric breaks breakbeat buddhism chicago code disco fediverse fiction funk furry house house music kubernetes lgbt linux logseq mastodon mental health movies music nixos obsidian personal philosophy pkm poetry prompt second life social software soul technology writing