How to Create a Virtual Machine Using KubeVirt – A Detailed Walkthrough

If you are looking for a way to deploy Virtual Machines, alongside your container workloads, then KubeVirt may be the answer. In this blog post we will explore the details on how to create a Virtual Machine using KubeVirt.

Platform9 Managed Kubernetes has a new UI for KubeVirt. The UI has been reworked and has added a Wizard that will guide you through creation, UI enhancements that show more details around Virtual Machines and Virtual Machine Instances, and additional options as well as overviews. 

KubeVirt helps users run Virtual Machines alongside their container based workloads. In this guide we will walk through the creation of a VM using the PMK UI, however you could also create a VM using kubectl.


Before we can walk through this guide we need to make sure a PMK environment has been setup with KubeVirt. After KubeVirt has been installed we need to install storage to back our Virtual Machine. In this demo I installed Ceph using Rook. Below are guides on how to do each, however if you get stuck we also have documentation that can be referenced: KubeVirt.




Starting out we have a new view for Virtual Machines and Virtual Machine Instances. There are a few differences between a VM and a VMI, but I usually think of it as a running Virtual Machine (VMI) vs a template of a Virtual Machine (VM). If you create a VMI then the instance will start running as soon as it has been setup, while a Virtual Machine will wait for you to start/stop/restart unless you have specified that it should always be running.

In this view we will end up selecting “New Virtual Machine” which is where we will actually define the Virtual Machine as a VM/VMI.

Here is a view of the VMI section, which is where we can view running Virtual Machines. Later in this demo we will show a running VMI.

KubeVirt Wizard

The wizard has a few sections and different options. We are going to walk through creating a VM with the Wizard, however if you wanted you could create a VM with YAML which will allow you to paste in your VM/VMI YAML configuration. If you already have working VMs and just want to create them in an environment managed by PMK, then you should use the YAML option.

Basic Settings

In this section we name the VM. A VM name can only use specific characters.

A lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, ‘-‘ or ‘.’, and must start and end with an alphanumeric character (e.g. ‘’, regex used for validation is ‘[a-z0-9]([-a-z0-9][a-z0-9])?(\.[a-z0-9]([-a-z0-9][a-z0-9])?)*’

If you have already created the backing storage for the VM then you will want to use the same namespace where the volume lives. In our example we are doing everything in the default namespace.

Select the cluster and then the namespace where you want to run your VM. Select the VM type and Run Strategy. In the demo we are deploying to the KubeVirt cluster, in the default namespace, with the type Virtual Machine, and the run strategy Manual. With the manual run strategy we can control when the Virtual Machine starts/stops/restarts.

KubeVirt - Creating a Virtual Machine Basic Settings


Storage can get complicated as there are a few different options. We will go over each, however the type used will depend on your use case.

Cloud Image

With a cloud image we can pull qcow2/raw images, similar to what you would use with OpenStack, and copy them into a volume. The volume will then back the VM pod. For this example we will select access mode ReadWriteOnce, set the size to 15GB, and the storage class to ceph-block. Ceph-Block will be setup by default if you are using Rook Ceph. If you are using a different type of storage backend then you’ll want to verify what access mode and storage class to use.

In the demo I am using a cirros image for simplicity. The cirros image will not require additional configuration with cloud-init to allow us to login. Other cloud images may require SSH keys for authentication, or specific options that enable password based authentication over SSH. SSH access can also be configured by modifying the YAML instead of using Cloud-Init.

      - sshPublicKey:
              secretName: my-pub-key

KubeVirt - Creating a VM using a Cloud Image

Repository Image

An image from a Repository is a container that has a cloud image expanded into it. This can be useful if you want to store the image in a public or private repository. We set the same details for Access Mode, Size, and Storage Class.

Example of Cirros in the KubeVirt DockerHub repository


Uploading an image can cut down on the time it takes to create a Virtual Machine, and also gives us a way to clone or attach an existing disk to our VM. In the examples for Clone and Attach Existing we are using a previously created data volume using the commands provided in the UI.

KubeVirt - virtctl image-upload examples

It is possible that you will run into an error message related to uploadproxy URL not found:

PVC default/demo-vm-dv not found 
DataVolume default/demo-vm-dv created
Waiting for PVC demo-vm-dv upload pod to be ready...
Pod now ready
uploadproxy URL not found

To resolve the issue you will want to find the endpoint with the following commands:

$ kubectl get service cdi-uploadproxy -n cdi
NAME              TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
cdi-uploadproxy   ClusterIP   <none>        443/TCP   19m

If you used Krew to install virtctl:

kubectl virt image-upload --pvc-name=demo-vm-pv --pvc-size=10Gi --image-path=cirros-0.5.1-x86_64-disk.img --uploadproxy-url= --insecure
kubectl virt image-upload dv demo-vm-dv --size=10Gi --image-path=cirros-0.5.1-x86_64-disk.img --uploadproxy-url= --insecure

With virtctl:

virtctl image-upload --pvc-name=demo-vm-pv --pvc-size=10Gi --image-path=cirros-0.5.1-x86_64-disk.img --uploadproxy-url= --insecure
virtctl image-upload dv demo-vm-dv --size=10Gi --image-path=cirros-0.5.1-x86_64-disk.img --uploadproxy-url= --insecure

If you don’t have access to the ClusterIP, update the service to type LoadBalancer or NodePort.

Clone Existing

With clone existing we can take the image that was uploaded to a volume and clone it. This option reduces the time it takes to download the image and expand it into a volume, which means faster VM creation. The time it takes to create the volume, download the image, and expand it into the volume will depend on your hardware and network speed. Since we are cloning the volume instead of using it, we can use the base volume to create additional VMs in the future.

Attach Existing

The alternative to cloning would be to use the volume we created. This will attach the volume, which can be useful if we only want to create a single Virtual Machine based off of the image we uploaded previously to a volume.


In this section we can specify VCPU and RAM resource requests. A preset can also be selected if one has been configured beforehand.


In the network section we can select between Masquerade (default) and Bridge. This section is more useful if you have configured networking, or have special networking needs.


Cloud-Init allows us to configure the Virtual Machine on launch. In this section we can specify user configurations, SSH keys, applications to install, and many other options. For our example, since we SSH using password based authentication, we have left the cloud-init section blank. In the prerequisites section, earlier in the blog, we have linked to a KubeVirt setup that provides YAML configurations using cloud-init in case you need to see examples.

Cloud-Init Docs –


The YAML resource shows us what is going to be created based on the selections we have made with the Wizard. In this section you can edit the YAML if you have additional requirements such as labeling.

KubeVirt - YAML Resource


Finally we have made it to the end of the Wizard. In this section there is a brief overview for review. Once the information has been reviewed we can select Create, and a VM will be created for us. Since we specified a Run Strategy of Manual we will need to start the VM after it has been created.

Virtual Machine Start

Now that the VM has been created it will be viewable in the Virtual Machines section. Select the VM and then select Start. This will start the VM which will create a new resource for us, the Virtual Machine Instance. The VMI will be viewable in the VMI section.

Virtual Machine Instances

Now that we have started the VM we can view it in the VM Instances section. This will give us an overview of the VM such as the status, the node where it is running, and other labels that were added. In this section we can also pause and unpause the VMI.

KubeVirt - Running VMI

Now that we have a Virtual Machine running we may want to access it. We can use the virt/virtctl CLI to expose the VM via a ClusterIP.

There are options to use a LoadBalancer, NodePort, or ClusterIP. ClusterIP is the default. To use any of the others we can specify –type TYPE.

kubectl virt expose vmi demo-vm --name demo-svc --port 22 --target-port 22

Once we have exposed the VM we can find the ClusterIP that was created with:

$ kubectl get service demo-svc
demo-svc   ClusterIP   <none>        22/TCP    110s

Now we have an IP address that can be used to SSH to the instance. The default user for cirros is cirros and the default password is gocubsgo.

$ ssh cirros@
cirros@'s password: 
$ uname
$ uptime
 20:13:20 up 23:22,  1 users,  load average: 0.00, 0.00, 0.00

Troubleshooting Tips

If a VM has been launched, but isn’t starting, we may want to check the volumes to verify that everything has been created and that the image has expanded into the volume.


We are using storage to back our VM which means that we need to verify that the storage is setup and working. Verify that the PV and PVC are both created and not in a blocking state (failed) and that they are bound.

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                      STORAGECLASS   REASON   AGE
pvc-f8064569-246e-4ef9-8ea4-78b17cb63821   10Gi       RWO            Delete           Bound    default/demo-test-disk-1   ceph-block              2m21s
$ kubectl get pvc
NAME               STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
demo-test-disk-1   Bound    pvc-f8064569-246e-4ef9-8ea4-78b17cb63821   10Gi       RWO            ceph-block     2m38s

If there are issues with the disks then you will want to run kubectl describe pv NAME to see the errors associated with it. Due to the amount of errors you could be seeing we will gloss over this section and defer to the storage providers documentation for the storage you are using.

Based on the examples above, the storage is setup and is working correctly. Let’s go ahead and check on the importer pod that expands the image specified into the volume we have created. We can run kubectl get pods to see the importer pod running, then run kubectl logs -f NAMEto see the logs/status of the disk creation and image conversion.

$ kubectl get pods
NAME                          READY   STATUS    RESTARTS   AGE
importer-demo-test-disk-1     1/1     Running   0          25s
$ kubectl logs -f importer-demo-test-disk-1
I1201 20:16:19.774363       1 importer.go:52] Starting importer
I1201 20:16:19.825284       1 importer.go:135] begin import process
I1201 20:16:21.055268       1 data-processor.go:323] Calculating available size
I1201 20:16:21.055353       1 data-processor.go:335] Checking out file system volume size.
I1201 20:16:21.055387       1 data-processor.go:343] Request image size not empty.
I1201 20:16:21.055437       1 data-processor.go:348] Target size 10447220736.
I1201 20:16:21.056862       1 util.go:39] deleting file: /data/lost+found
I1201 20:16:21.223388       1 nbdkit.go:269] Waiting for nbdkit PID.
I1201 20:16:22.223901       1 nbdkit.go:290] nbdkit ready.
I1201 20:16:22.223967       1 data-processor.go:231] New phase: Convert
I1201 20:16:22.223992       1 data-processor.go:237] Validating image
I1201 20:16:24.731156       1 qemu.go:250] 0.00
I1201 20:16:26.730825       1 qemu.go:250] 1.25
I1201 20:17:29.001141       1 qemu.go:250] 99.69
I1201 20:17:30.727739       1 data-processor.go:231] New phase: Resize
W1201 20:17:30.774162       1 data-processor.go:310] Available space less than requested size, resizing image to available space 9872623104.
I1201 20:17:30.774296       1 data-processor.go:316] Expanding image size to: 9872623104
I1201 20:17:30.830524       1 data-processor.go:237] Validating image
I1201 20:17:30.841875       1 data-processor.go:231] New phase: Complete
I1201 20:17:30.842315       1 importer.go:217] Import Complete

After the disk has been created we can start the VM.


The new Wizard makes creating Virtual Machines easier by taking some of the guesswork out of creating a VM YAML by hand. We hope the new UI and Wizard workflow will make KubeVirt more accessible to everyone. If you have any feedback or questions – please join our Slack Community.

Mike Petersen

You may also enjoy

VMware vs OpenStack: Why OpenStack is the better VMware alternative in 2024

By Kamesh Pemmaraju

Platform9 5.5 – It’s Time to Focus on Making Cloud Native Easy

By Chris Jones

The browser you are using is outdated. For the best experience please download or update your browser to one of the following:

State of Kubernetes FinOps Survey – Win 13 prizes including a MacBook Air.Start Now