Technical architectural analysis, practical configuration manifests, and deployment instructions for Harvester HCI in HomeLab: KubeVirt VMs.
Installation
HCI: Host
Hosts โ \
Mount Disk and Join Longhorn
- Storage โ Click Add Disk โ Select Disk โ Save
- Loaded when Longhorn schedule ends
HCI: Creation
Manage SSH Keys
- Make sure you have the private key, just register the public key
Upload Image
Images โ Create
Cloud Image repositories
- Windows
- Windows Server 2022 | Microsoft Evaluation Center
- Windows Server 2025 | Microsoft Evaluation Center
- Virtio-win
- Index of /groups/virt/virtio-win/direct-downloads
- Ubuntu
- ISO: ubuntu-releases
- QCow2: Ubuntu Cloud Images โ the official Ubuntu images for public clouds, Openstack, KVM and LXD
- Talos
- Image Factory
- Nocloud โ Sidero Documentation
- System expansion
siderolabs/iscsi-toolssiderolabs/qemu-guest-agentsiderolabs/util-linux-tools- Kernel arguments
talos.platform=kvm console=ttyS0,115200n8- customization-all
customization:
extraKernelArgs:
- talos.platform=kvm
- console=ttyS0,115200n8
systemExtensions:
officialExtensions:
- siderolabs/iscsi-tools
- siderolabs/qemu-guest-agent
- siderolabs/util-linux-tools
Upload image via URL
-
.iso/.qcow/.qcow2/.raw/.imgCopy image link -
Insert into Harvester and specify
Name. (automatic labeling) โCreate -
Select Storage Class
- Note: Creating a Storage Class for Standalone
-
Confirm Download
Create Bootable Volume
Created as PVC on host kubernetes (pre-bounded)
VM: Windows VDI Server Setup
Windows does not have a VirtIo type volume driver in the default image, so mount the virtio-win image as well. (Virtio-win)
Windows Server 2025 Installation
-
Basics
-
Volumes
-
WebVNC access
-
After setting up Windows appropriately, select Desktop Experience in the edition selection window. (GUI)
- After this, the
Location to Install Windows Serverscreen appears -
Click
Load Driver -
virtio-windisk โ amd64 โ select latest drive -
Select
Red Hat VirtIO SCSI controllerand thenInstall -
Install on VirtIO disk, which can now be viewed easily
Network Setup
Get-PSDrive -PSProvider FileSystem #ํ์ธ๋ virtio drive=E:
pnputil /add-driver "E:\NetKVM\2k25\amd64\*.inf" /install
# ์ธํฐ๋ท ์ฐ๊ฒฐ๋ฐ ipํ ๋น ํ์ธ
ipconfig
ping 8.8.8.8
All-in-one Virtio Drivers Setup
- Install
virtio-win-gt-x64.msi
Check in device manager
Enable RDP
- Click the
Startbutton > Go toSettings>System> **Remote Desktop**. - Change the toggle to โOnโ.
Service exposure/access
RDP default port: 3389
Default access account: Administrator
- LoadBalancer
Remove boot disk and driver disk
- VM: Volume Unmount and Removal
VM: Ubuntu Server
Configuration
Service exposure/access
SSH default port: 22
Default access account: ubuntu
Mount disk
# lvm์์ด ext4
sudo mkfs.ext4 /dev/vdb
sudo mkdir /workspace
sudo mount /dev/vdb /workspace
Network
Cluster Network
Understanding Harvester Network Options
- Cluster Network โ Masquerade (L3)
-
Because Masquerade mode does not expose the VM to the external network, NAT must be configured with
LoadBalancersettings to access the service from outside -
Cluster Network โ Bridge (L2)
- Bridge mode exposes the VM to the upstream router so it can receive DHCP assignment.
Create VM Network
Understanding Harvester Network Options
- VM Network โ Bridge (L2)
- Bridge mode exposes the VM to the upstream router so it can receive DHCP assignment.
| Harvester Terminology | Standard network terminology | Core Mechanism |
| Untagged | Standard L2 LAN | Direct communication with physical switch (Access Port) without any tag on packets |
| L2VLAN | 802.1Q VLAN | Communicates with physical switch (trunk port) by attaching VLAN ID (Tag) to packet header |
| Overlay | VXLAN | Encapsulate and transmit L2 packets with UDP (L3/L4) packets |
- untagged: Very simple
LoadBalancer
โ DHCP: Host-dependent
โ VM NetworksIP Pools: LoadBalancer โ IP Pools โ VM Networks (VLAN or Else)
-
Service Port Forwarding
-
Target VM label selection
HCI: Management
VM: Resizing CPU / Memory
- edit config
VM: Resizing Disk
If LVM is configured, it is better to add a new disk and combine it logically
VM: Volume Unmount and Removal
-
Edit Config
-
Remove Drive image or installation image
Features
Create Storage Class for Standalone
Harvester’s default Longhorn configuration is Replicas=3, but since replica schedules are not possible on a single node, Volume degreaded occurs when creating a VM.
-
Instant recovery of volumes currently degreaded
1. Access Longhorn Dashboard
2. In Volume settings, change Update Replicas Count:1 -
Existing SCs such as
harvester-longhorncannot be modified, so create a new SC -
Number of Replica =
1 -
set Default
Enable Rancher & Longhorn UI
Supports use of built-in Rancher and Longhorn dashboards for debugging and validation purposes only
- Preference > Advanced Features >
Enable Extension developer features
- Support
Enable Vim Key Mapping
- Preference > YAML Editor Setting >
Vim
GitOps
Rancher Connection
-
Rancher
-
Harvester
Solution
Expand details
Get "https://rancher.mgnt.fredric18.uk/v3/import/6v5bg947vq7zm2tb8wjxbbxp9tgq8w6rcx7bskdd6hw4h2lt4r4gcp_c-dm6zk.yaml": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
โ In Harvester CLI DNS Setting
# Unbound ๋์ ํ์ธ
rancher@fredric18:~> nslookup rancher.mgnt.fredric18.uk 192.168.1.1
Server: 192.168.1.1
Address: 192.168.1.1#53
Name: rancher.mgnt.fredric18.uk
Address: 192.168.118.13
# CoreDNS์ Unbound ์ ์ฉ
k exec -it -n harvester-system deployments/harvester --container apiserver -- curl -kv https://rancher.mgnt.fredric18.uk/v3/import/6v5bg947vq7zm2tb8wjxbbxp9tgq8w6rcx7bskdd6hw4h2lt4r4gcp_c-dm6zk.yaml
>>>
* Could not resolve host: rancher.mgnt.fredric18.uk
* shutting down connection #0
curl: (6) Could not resolve host: rancher.mgnt.fredric18.uk
command terminated with exit code 6
<<<
k get cm -n kube-system -l k8s-app=kube-dns
k edit cm -n kube-system rke2-coredns-rke2-coredns
>>>edit
forward . 192.168.1.1 8.8.8.8
<<<
k delete pods -n kube-system -l k8s-app=kube-dns
k exec -it -n harvester-system deployments/harvester --container apiserver -- curl -kv https://rancher.mgnt.fredric18.uk/v3/import/6v5bg947vq7zm2tb8wjxbbxp9tgq8w6rcx7bskdd6hw4h2lt4r4gcp_c-dm6zk.yaml
>>>
* Host rancher.mgnt.fredric18.uk:443 was resolved.
* IPv6: (none)
* IPv4: 192.168.118.13
* Trying 192.168.118.13:443...
<<<
CLI
Generate VM
Masquerade mode
SNAT vs Masquerade
- SNAT (Source Network Address Translation): A higher-level concept that collectively refers to all technologies that change the source IP. It is common for administrators to explicitly specify the destination IP to be converted.
- Masquerade: A dynamic version of SNAT. Even if the administrator does not specify the IP individually, the system automatically detects the IP of the network interface where the packet goes out and applies it as the source IP.
VMSet + LB for SSH
apiVersion: loadbalancer.harvesterhci.io/v1beta1
kind: LoadBalancer
metadata:
annotations:
loadbalancer.harvesterhci.io/namespace: harvester-public
loadbalancer.harvesterhci.io/network: ""
loadbalancer.harvesterhci.io/project: ""
finalizers:
- wrangler.cattle.io/harvester-lb-controller
name: cks-inkang-01
namespace: harvester-public
spec:
backendServerSelector:
harvesterhci.io/vmName:
- cks-inkang-01
healthCheck: {}
ipam: dhcp
listeners:
- backendPort: 22
name: ssh
port: 22
protocol: TCP
workloadType: vm
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
annotations:
volume.beta.kubernetes.io/storage-provisioner: driver.longhorn.io
volume.kubernetes.io/storage-provisioner: driver.longhorn.io
finalizers:
- kubernetes.io/pvc-protection
- wrangler.cattle.io/persistentvolumeclaim-controller
name: cks-inkang-01-root
namespace: harvester-public
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
storageClassName: harvester-longhorn-standalone
volumeMode: Block
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
annotations:
harvesterhci.io/imageId: harvester-public/image-jb4b8
volume.beta.kubernetes.io/storage-provisioner: driver.longhorn.io
volume.kubernetes.io/storage-provisioner: driver.longhorn.io
finalizers:
- kubernetes.io/pvc-protection
- wrangler.cattle.io/persistentvolumeclaim-controller
name: cks-inkang-01-bootable
namespace: harvester-public
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
storageClassName: longhorn-image-jb4b8
volumeMode: Block
---
apiVersion: v1
data:
networkdata: ""
userdata: I2Nsb3VkLWNvbmZpZwpwYWNrYWdlX3VwZGF0ZTogdHJ1ZQpwYWNrYWdlczoKICAtIHFlbXUtZ3Vlc3QtYWdlbnQKcnVuY21kOgogIC0gLSBzeXN0ZW1jdGwKICAgIC0gZW5hYmxlCiAgICAtIC0tbm93CiAgICAtIHFlbXUtZ3Vlc3QtYWdlbnQuc2VydmljZQpzc2hfYXV0aG9yaXplZF9rZXlzOgogIC0gc3NoLWVkMjU1MTkKICAgIEFBQUFDM056YUMxbFpESTFOVEU1QUFBQUlLSkxpb0tESFZqRUtnTFBwbEpkdVh4YzlHR0xaa0RtaVUwNDlRRFFEYTd1CiAgICBhZG1pbkBtYWlsLmZyZWRyaWMxOC51awoKcGFzc3dvcmQ6ICJpbmthbmcxMjMjIgpjaHBhc3N3ZDoKICBsaXN0OiB8CiAgICByb290Omlua2FuZzEyMyMKICAgIHVidW50dTppbmthbmcxMjMjCiAgZXhwaXJlOiBGYWxzZQpzc2hfcHdhdXRoOiBUcnVlCg==
kind: Secret
metadata:
labels:
harvesterhci.io/cloud-init-template: harvester
name: cks-inkang-01
namespace: harvester-public
type: secret
---
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
annotations:
harvesterhci.io/vmRunStrategy: RerunOnFailure
kubevirt.io/latest-observed-api-version: v1
kubevirt.io/storage-observed-api-version: v1
network.harvesterhci.io/ips: '[]'
template-version.harvesterhci.io/customName: QK9h2nZlKw
creationTimestamp: "2026-04-17T06:03:53Z"
finalizers:
- kubevirt.io/virtualMachineControllerFinalize
- wrangler.cattle.io/VMController.CleanupPVCAndSnapshot
generation: 1
labels:
harvesterhci.io/creator: harvester
harvesterhci.io/os: ubuntu
template.harvesterhci.io/templateID: ubuntu-tiny
name: cks-inkang-01
namespace: harvester-public
spec:
runStrategy: RerunOnFailure
template:
metadata:
annotations:
harvesterhci.io/sshNames: '["harvester-public/admin"]'
creationTimestamp: null
labels:
harvesterhci.io/vmName: cks-inkang-01
harvesterhci.io/vmNamePrefix: cks-inkang
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels:
harvesterhci.io/vmNamePrefix: cks-inkang
topologyKey: kubernetes.io/hostname
weight: 1
architecture: amd64
domain:
cpu:
cores: 1
maxSockets: 1
sockets: 1
threads: 1
devices:
disks:
- bootOrder: 1
disk:
bus: virtio
name: bootable
- bootOrder: 2
disk:
bus: virtio
name: root
- disk:
bus: virtio
name: cloudinitdisk
inputs:
- bus: usb
name: tablet
type: tablet
interfaces:
- masquerade: {}
model: virtio
name: default
features:
acpi:
enabled: true
machine:
type: q35
memory:
guest: 4Gi
resources:
limits:
cpu: "1"
memory: 2Gi
requests:
cpu: 125m
memory: 2730Mi
evictionStrategy: LiveMigrateIfPossible
networks:
- name: default
pod: {}
terminationGracePeriodSeconds: 120
volumes:
- name: bootable
persistentVolumeClaim:
claimName: cks-inkang-01-bootable
- name: root
persistentVolumeClaim:
claimName: cks-inkang-01-root
- cloudInitNoCloud:
networkDataSecretRef:
name: cks-inkang-01
secretRef:
name: cks-inkang-01
name: cloudinitdisk
Bridge mode
Bridge does not manage the VM IP in Harvester, but requests allocation from the upstream DHCP server, so it is possible to allocate an IP at the same level as Harvester.
Generally, a vLAN interface is created and a NetworkAttachmentDefinition is declared.
Upstream DHCP vLAN Interface
Bridge Network configuration (NetworkAttachmentDefinition)
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
name: vlan1
namespace: harvester-public
annotations:
network.harvesterhci.io/clusternetwork: mgmt
spec:
config: '{
"cniVersion": "0.3.1",
"name": "vlan1",
"type": "bridge",
"bridge": "mgmt-br",
"vlan": 30
}'
โ clusterNetwork (static)
apiVersion: v1
items:
- apiVersion: network.harvesterhci.io/v1beta1
kind: ClusterNetwork
metadata:
creationTimestamp: "2025-12-07T14:48:39Z"
finalizers:
- wrangler.cattle.io/harvester-network-manager-cn-controller
generation: 1
name: mgmt
resourceVersion: "4969"
uid: 1c602bce-a7c2-4fa5-8a28-95d17af5ca25
status:
conditions:
- lastUpdateTime: "2025-12-07T14:48:39Z"
status: "True"
type: ready
kind: List
metadata:
resourceVersion: ""
VMSet
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
annotations:
volume.beta.kubernetes.io/storage-provisioner: driver.longhorn.io
volume.kubernetes.io/storage-provisioner: driver.longhorn.io
finalizers:
- kubernetes.io/pvc-protection
- wrangler.cattle.io/persistentvolumeclaim-controller
name: cks-inkang-03-root
namespace: harvester-public
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
storageClassName: harvester-longhorn-standalone
volumeMode: Block
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
annotations:
harvesterhci.io/imageId: harvester-public/image-jb4b8
volume.beta.kubernetes.io/storage-provisioner: driver.longhorn.io
volume.kubernetes.io/storage-provisioner: driver.longhorn.io
finalizers:
- kubernetes.io/pvc-protection
- wrangler.cattle.io/persistentvolumeclaim-controller
name: cks-inkang-03-bootable
namespace: harvester-public
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
storageClassName: longhorn-image-jb4b8
volumeMode: Block
---
apiVersion: v1
data:
networkdata: ""
userdata: I2Nsb3VkLWNvbmZpZwpwYWNrYWdlX3VwZGF0ZTogdHJ1ZQpwYWNrYWdlczoKICAtIHFlbXUtZ3Vlc3QtYWdlbnQKcnVuY21kOgogIC0gLSBzeXN0ZW1jdGwKICAgIC0gZW5hYmxlCiAgICAtIC0tbm93CiAgICAtIHFlbXUtZ3Vlc3QtYWdlbnQuc2VydmljZQpzc2hfYXV0aG9yaXplZF9rZXlzOgogIC0gc3NoLWVkMjU1MTkKICAgIEFBQUFDM056YUMxbFpESTFOVEU1QUFBQUlLSkxpb0tESFZqRUtnTFBwbEpkdVh4YzlHR0xaa0RtaVUwNDlRRFFEYTd1CiAgICBhZG1pbkBtYWlsLmZyZWRyaWMxOC51awoKcGFzc3dvcmQ6ICJpbmthbmcxMjMjIgpjaHBhc3N3ZDoKICBsaXN0OiB8CiAgICByb290Omlua2FuZzEyMyMKICAgIHVidW50dTppbmthbmcxMjMjCiAgZXhwaXJlOiBGYWxzZQpzc2hfcHdhdXRoOiBUcnVlCg==
kind: Secret
metadata:
labels:
harvesterhci.io/cloud-init-template: harvester
name: cks-inkang-03
namespace: harvester-public
type: secret
---
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
annotations:
harvesterhci.io/vmRunStrategy: RerunOnFailure
kubevirt.io/latest-observed-api-version: v1
kubevirt.io/storage-observed-api-version: v1
network.harvesterhci.io/ips: '[]'
template-version.harvesterhci.io/customName: QK9h2nZlKw
creationTimestamp: "2026-04-17T06:03:53Z"
finalizers:
- kubevirt.io/virtualMachineControllerFinalize
- wrangler.cattle.io/VMController.CleanupPVCAndSnapshot
generation: 1
labels:
harvesterhci.io/creator: harvester
harvesterhci.io/os: ubuntu
template.harvesterhci.io/templateID: ubuntu-tiny
name: cks-inkang-03
namespace: harvester-public
spec:
runStrategy: RerunOnFailure
template:
metadata:
annotations:
harvesterhci.io/sshNames: '["harvester-public/admin"]'
creationTimestamp: null
labels:
harvesterhci.io/vmName: cks-inkang-03
harvesterhci.io/vmNamePrefix: cks-inkang
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels:
harvesterhci.io/vmNamePrefix: cks-inkang
topologyKey: kubernetes.io/hostname
weight: 1
architecture: amd64
domain:
cpu:
cores: 1
maxSockets: 1
sockets: 1
threads: 1
devices:
disks:
- bootOrder: 1
disk:
bus: virtio
name: bootable
- bootOrder: 2
disk:
bus: virtio
name: root
- disk:
bus: virtio
name: cloudinitdisk
inputs:
- bus: usb
name: tablet
type: tablet
interfaces:
- bridge: {}
model: virtio
name: default
features:
acpi:
enabled: true
machine:
type: q35
memory:
guest: 4Gi
resources:
limits:
cpu: "1"
memory: 2Gi
requests:
cpu: 125m
memory: 2730Mi
evictionStrategy: LiveMigrateIfPossible
networks:
- name: default
multus:
networkName: harvester-public/vlan1
terminationGracePeriodSeconds: 120
volumes:
- name: bootable
persistentVolumeClaim:
claimName: cks-inkang-03-bootable
- name: root
persistentVolumeClaim:
claimName: cks-inkang-03-root
- cloudInitNoCloud:
networkDataSecretRef:
name: cks-inkang-03
secretRef:
name: cks-inkang-03
name: cloudinitdisk
VM Diffs with Masquerade VS Bridge
# ... (์๋ถ๋ถ ์๋ต)
spec:
# ...
networks:
- name: default
# pod: {} <-- ์ด ๋ถ๋ถ์ ์ง์ฐ๊ณ ์๋ multus๋ฅผ ์ถ๊ฐํฉ๋๋ค.
multus:
networkName: harvester-public/vlan1 # <-- ์ค์ ์ฌ์ฉํ๋ ค๋ ๋คํธ์ํฌ ์ด๋ฆ (Namespace/Name)
domain:
# ...
devices:
interfaces:
- bridge: {} # <-- masquerade์์ bridge๋ก ๋ณ๊ฒฝ
model: virtio
name: default
# ... (๋ท๋ถ๋ถ ์๋ต)
Backup
# vm ๋ชฉ๋ก
k get pods -n harvester-public
# virsh vm listing
k exec -it virt-launcher-management-00-kh82s -n harvester-public -- virsh list --all
# dump
k exec -it virt-launcher-management-00-kh82s -n harvester-public -- virsh dumpxml harvester-public_management-00 > harvester-public_management-00.xml
# ๋์คํฌ ๊ฒฝ๋ก ํ์ธ
k exec -it virt-launcher-management-00-kh82s -n harvester-public -- virsh domblklist
Add-ons
Advanced โ Add-ons
Observability
Logging
Monitoring
-
Go to Prometheus UI
-
Go to Grafana dashboard
-
For the purpose of searching without logging in, or if development is necessary, check the initial password
# Grafana admin ํจ์ค์๋ ํ์ธ
kubectl get secret -n cattle-monitoring-system rancher-monitoring-grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo
- Change Password to be the same as Harvester




Leave a Reply