Creating VMs from CLI Using YAML Manifests
Overview
This tutorial demonstrates how to create virtual machines using the OpenShift CLI (oc) and YAML manifests. CLI-based VM creation is the foundation for automation, GitOps, and Infrastructure as Code workflows. Unlike the web console approach, YAML manifests provide version control, repeatability, and integration with CI/CD pipelines.
What You’ll Learn
-
Generating VM manifests using
virtctl create vmand customizing before applying -
VirtualMachine YAML manifest structure (apiVersion, kind, metadata, spec)
-
Creating VMs with containerDisk for testing (no persistent storage)
-
Creating VMs with DataVolume boot sources for production workloads
-
Configuring CPU, memory, and disk resources
-
Using
runStrategyinstead of the deprecatedrunningfield -
Integrating cloud-init for user setup and SSH key injection
-
Applying manifests with
oc apply -f -
Verifying VM creation and accessing VMs via console and SSH
Prerequisites
-
OpenShift 4.18+ with OpenShift Virtualization operator installed
-
OpenShift CLI (
oc) and virtctl installed (see Using virtctl for VM Management) -
A default StorageClass configured for VM disk provisioning
-
Available boot source images (DataSources) in the cluster
Verify boot sources are available:
oc get datasources -n openshift-virtualization-os-images
Verify your cluster has a default StorageClass:
oc get storageclass
Look for a StorageClass marked with (default). If none exists, see Configuring Default Storage Class.
Anatomy of a VirtualMachine YAML Manifest
A VirtualMachine resource in OpenShift Virtualization follows standard Kubernetes resource structure. Here’s the basic anatomy:
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: my-vm
namespace: demo
spec:
runStrategy: Always (1)
template:
metadata:
labels:
kubevirt.io/domain: my-vm
spec:
domain:
devices:
disks:
- name: rootdisk
interfaces:
- name: default
resources:
requests:
memory: 2Gi
cpu: 1
networks:
- name: default
volumes:
- name: rootdisk
dataVolumeTemplates: (2)
- metadata:
name: rootdisk-dv
| 1 | Lifecycle policy - Always, RerunOnFailure, Manual, or Halted |
| 2 | Optional DataVolume templates for creating persistent disks |
Use runStrategy instead of the deprecated running: true/false field. The runStrategy provides more granular lifecycle control.
|
Step 1: Generate a VM Manifest with virtctl
The virtctl create vm command can generate a base YAML manifest that you can customize before applying. This is often easier than writing manifests from scratch.
Generate Base Manifest
Use virtctl create vm to generate a manifest with a DataVolume boot source:
virtctl create vm --name=my-rhel-vm \
--volume-import type:ds,src:openshift-virtualization-os-images/rhel9 \
--memory=2Gi \
--user=cloud-user \
--ssh-key="$(cat ~/.ssh/id_rsa.pub)" > my-rhel-vm.yaml
This command:
-
Creates a VM named
my-rhel-vm -
Imports from the RHEL 9 DataSource as the boot volume
-
Sets memory to 2Gi (CPU allocation is handled via instancetypes or manual manifest editing)
-
Creates a cloud-init user named
cloud-user -
Injects your SSH public key for passwordless access
-
Outputs the YAML manifest to
my-rhel-vm.yaml(virtctl outputs YAML by default)
If you don’t have an SSH key, generate one with ssh-keygen -t ed25519 -C "your.email@example.com".
|
Review the generated manifest:
cat my-rhel-vm.yaml
Customize the Generated Manifest
The generated manifest provides a good starting point, but you’ll likely want to customize it. The virtctl command already created basic cloud-init configuration, but you may want to add more customizations:
-
Add SSH keys and passwords to cloud-init
-
Adjust CPU and memory allocations
-
Install additional packages
-
Add labels and annotations
Use your preferred text editor to customize the manifest. You can use vi:
vi my-rhel-vm.yaml
Or use nano if you prefer:
nano my-rhel-vm.yaml
Replace AAAA…your-public-key… with your actual SSH public key from ~/.ssh/id_ed25519.pub or similar.
|
Apply the Customized Manifest
Once you’ve customized the manifest, apply it to create the VM:
oc apply -f my-rhel-vm.yaml
Monitor the VM creation:
oc get vm my-rhel-vm -n demo -w
This workflow of generate → customize → apply is the most common pattern for creating VMs with YAML manifests in production environments.
Step 2: Create a Minimal VM with containerDisk
The simplest VM uses a containerDisk for the boot source. containerDisks are ephemeral (no persistent storage) but ideal for testing and development.
Project Setup
We’ll continue using the demo project created in Step 1. If you’re starting from this step specifically, create the project with oc new-project demo.
|
Apply the Minimal VM Manifest
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: vm-minimal
namespace: demo
spec:
runStrategy: Always
template:
metadata:
labels:
kubevirt.io/domain: vm-minimal
spec:
domain:
devices:
disks:
- name: rootdisk
disk:
bus: virtio
- name: cloudinitdisk
disk:
bus: virtio
interfaces:
- name: default
masquerade: {}
resources:
requests:
memory: 1Gi
networks:
- name: default
pod: {}
volumes:
- name: rootdisk
containerDisk: (1)
image: quay.io/containerdisks/fedora:latest
- name: cloudinitdisk
cloudInitNoCloud:
userData: |
#cloud-config
user: fedora
password: fedora123
chpasswd:
expire: false
ssh_pwauth: true
| 1 | containerDisk volume type - pulls a disk image from a container registry |
Apply the manifest:
oc apply -f vm-minimal-containerdisk.yaml
Verify VM Creation
Check the VM status:
oc get vm vm-minimal -n demo
Expected output:
NAME AGE STATUS READY
vm-minimal 30s Running True
Check the VirtualMachineInstance (the running VM):
oc get vmi vm-minimal -n demo
Expected output:
NAME AGE PHASE IP NODENAME READY
vm-minimal 1m Running 10.128.1.123 worker-node-2 True
Since this VM uses a containerDisk, no DataVolume is created. The disk image is pulled directly from the container registry.
Access the VM Console
Access the VM console to verify it’s working:
virtctl console vm-minimal -n demo
Log in with the credentials from cloud-init:
-
Username:
fedora -
Password:
fedora123
To exit the console session, press Ctrl+] or use the console escape sequence.
| ContainerDisk VMs boot quickly since no disk provisioning is required. However, any changes made to the disk are lost when the VM is stopped. |
Step 3: Create a VM with DataVolume Boot Source
For production workloads, use DataVolume boot sources that create persistent disks. DataVolumes clone from existing disk images and provide persistent storage that survives VM restarts.
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: vm-datavolume
namespace: demo
spec:
runStrategy: Always
template:
metadata:
labels:
kubevirt.io/domain: vm-datavolume
spec:
domain:
devices:
disks:
- name: rootdisk
disk:
bus: virtio
- name: cloudinitdisk
disk:
bus: virtio
interfaces:
- name: default
masquerade: {}
resources:
requests:
memory: 2Gi
cpu: 1
networks:
- name: default
pod: {}
volumes:
- name: rootdisk
dataVolume: (1)
name: vm-datavolume
- name: cloudinitdisk
cloudInitNoCloud:
userData: |
#cloud-config
ssh_pwauth: true
user:
name: fedora
lock_passwd: false
plain_text_passwd: fedora123
ssh_authorized_keys:
- 'ssh-ed25519 AAAA...your-public-key... user@workstation'
packages:
- vim
- git
runcmd:
- echo "VM setup complete" > /tmp/vm-ready
dataVolumeTemplates: (2)
- metadata:
name: vm-datavolume
spec:
sourceRef:
kind: DataSource
name: fedora
namespace: openshift-virtualization-os-images
storage:
resources:
requests:
storage: 30Gi
| 1 | Reference to a DataVolume instead of a containerDisk |
| 2 | DataVolume template creates a persistent disk from the Fedora DataSource |
Replace AAAA…your-public-key… with your actual SSH public key from ~/.ssh/id_ed25519.pub or similar.
|
Apply the manifest:
oc apply -f vm-datavolume.yaml
Monitor DataVolume Provisioning
DataVolume VMs take longer to start because the disk must be provisioned first. Monitor the process:
oc get dv vm-datavolume -n demo -w
Expected progression:
NAME PHASE PROGRESS RESTARTS AGE
vm-datavolume Pending 5s
vm-datavolume CloneInProgress 0.00% 10s
vm-datavolume CloneInProgress 45.2% 30s
vm-datavolume Succeeded 45s
Press Ctrl+C to stop watching once the DataVolume shows Succeeded.
Check the VM status:
oc get vm vm-datavolume -n demo
Step 4: Customize CPU, Memory, and Advanced Cloud-init
This example demonstrates more advanced VM configuration with specific CPU/memory settings and comprehensive cloud-init setup.
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: vm-customized
namespace: demo
spec:
runStrategy: Always
template:
metadata:
labels:
kubevirt.io/domain: vm-customized
spec:
domain:
cpu:
cores: 2 (1)
devices:
disks:
- name: rootdisk
disk:
bus: virtio
- name: cloudinitdisk
disk:
bus: virtio
interfaces:
- name: default
masquerade: {}
machine:
type: pc-q35-rhel9.4.0
memory:
guest: 4Gi (2)
resources:
requests:
memory: 4Gi
cpu: 2
networks:
- name: default
pod: {}
volumes:
- name: rootdisk
dataVolume:
name: vm-customized
- name: cloudinitdisk
cloudInitNoCloud:
userData: |
#cloud-config
ssh_pwauth: true
users: (3)
- name: fedora
lock_passwd: false
plain_text_passwd: s3cur3p4ss
ssh_authorized_keys:
- 'ssh-ed25519 AAAA...your-public-key... user@workstation'
- name: admin
groups: wheel
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- 'ssh-ed25519 AAAA...your-public-key... user@workstation'
packages:
- vim
- git
- tmux
- htop
write_files:
- path: /etc/motd
content: |
Welcome to your customized VM!
Created via YAML manifest and oc apply.
permissions: '0644'
runcmd:
- systemctl enable --now cockpit.socket
- echo "VM initialization complete at $(date)" > /var/log/vm-init.log
dataVolumeTemplates:
- metadata:
name: vm-customized
spec:
sourceRef:
kind: DataSource
name: fedora
namespace: openshift-virtualization-os-images
storage:
resources:
requests:
storage: 50Gi
| 1 | Specific CPU core count |
| 2 | Guest memory allocation (should match resources.requests.memory) |
| 3 | Additional user creation with sudo privileges |
The machine type pc-q35-rhel9.4.0 may vary by cluster version. Use oc get VirtualMachineClusterInstancetypes to see available types on your cluster.
|
Apply the customized VM:
oc apply -f vm-customized.yaml
Verify Advanced Configuration
Check the VM’s CPU and memory allocation:
oc describe vmi vm-customized -n demo | grep -A 5 "Requests"
Access the VM to verify cloud-init ran successfully:
virtctl ssh admin@vm-customized -n demo
Check the message of the day that was written by cloud-init:
cat /etc/motd
Verify tmux was installed:
which tmux
Verify htop was installed:
which htop
Check the initialization log:
cat /var/log/vm-init.log
Step 5: VM Lifecycle Operations
Check VM Status and Events
View detailed VM information:
oc describe vm vm-customized -n demo
Check VM events for troubleshooting:
oc get events -n demo --field-selector involvedObject.name=vm-customized
Stop and Start VMs
Stop a VM (graceful shutdown):
virtctl stop vm-customized -n demo
Verify it’s stopped:
oc get vm vm-customized -n demo
Start the VM again:
virtctl start vm-customized -n demo
Update VM Configuration
You can modify most VM settings by editing the manifest and reapplying it. For example, to change memory:
oc patch vm vm-customized -n demo --type merge --patch '{"spec":{"template":{"spec":{"domain":{"memory":{"guest":"6Gi"},"resources":{"requests":{"memory":"6Gi"}}}}}}}'
| Memory and CPU changes require a VM restart to take effect. |
Troubleshooting
VM Won’t Start
Check the VirtualMachine events:
oc describe vm <vm-name> -n demo
Also check the VirtualMachineInstance events:
oc describe vmi <vm-name> -n demo
Common issues:
-
DataVolume provisioning failed (check
oc get dv) -
Insufficient cluster resources (check node capacity)
-
Invalid cloud-init syntax (check pod logs)
DataVolume Provisioning Issues
Check DataVolume status:
oc describe dv <datavolume-name> -n demo
Check the CDI import pod logs:
oc logs -l app=containerized-data-importer -n openshift-cnv
Cloud-init Errors
Access the VM console and check cloud-init status:
virtctl console <vm-name> -n demo
Log in to the VM and check cloud-init status. To exit the console session, press Ctrl+].
Inside the VM, check cloud-init status:
cloud-init status --long
Check cloud-init logs for errors:
cat /var/log/cloud-init.log
For detailed cloud-init troubleshooting, see Cloud-init Fundamentals for OpenShift Virtualization VMs.
Cleanup
Remove all VMs and associated resources:
oc delete vm vm-minimal vm-datavolume vm-customized -n demo
Wait a moment for DataVolumes to be cleaned up, then remove the project:
oc delete project demo
| Deleting the project removes all VMs and their persistent disks. Ensure you have backups if needed. |
Summary
You learned how to:
-
Generate VM manifests using
virtctl create vmwith volume imports from DataSources -
Customize generated manifests before applying them (the recommended workflow)
-
Structure VirtualMachine YAML manifests with proper apiVersion, metadata, and spec sections
-
Create lightweight VMs using containerDisk for testing scenarios
-
Create production VMs with DataVolume boot sources for persistent storage
-
Configure CPU cores, memory allocation, and disk sizes
-
Use
runStrategyfor VM lifecycle management instead of deprecatedrunningfield -
Integrate cloud-init for automated user setup, SSH key injection, and package installation
-
Apply manifests with
oc apply -fand verify creation withoc get vm,oc get vmi,oc get dv -
Access VMs via
virtctl consoleandvirtctl ssh -
Perform basic lifecycle operations (stop, start, restart)
-
Clean up resources properly
See Also
-
Cloud-init Fundamentals for OpenShift Virtualization VMs - Deep dive into cloud-init configuration options
-
Configuring Static IPs for VMs - Configure static IP addresses for VMs
-
VM Node Placement with Selectors and Affinity Rules - Control which nodes run your VMs
-
Multi-Disk VM Configuration with Different Storage Classes - Advanced storage configuration for VMs