Harvester HCI in HomeLab: KubeVirt VMs

๐Ÿ’ก Harvester HCI in HomeLab: KubeVirt VMs โ€” Executive Summary

Technical architectural analysis, practical configuration manifests, and deployment instructions for Harvester HCI in HomeLab: KubeVirt VMs.

Installation

HCI: Host

References and Guidelines (NOTE)

Hosts โ†’ \ โ†’ Click Edit Config

Mount Disk and Join Longhorn
  1. 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
References and Guidelines (NOTE)

Images โ†’ Create

Cloud Image repositories
bash
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
  1. .iso / .qcow / .qcow2 / .raw / .img Copy image link

  2. Insert into Harvester and specify Name. (automatic labeling) โ†’ Create

  3. Select Storage Class

    1. Note: Creating a Storage Class for Standalone
  4. Confirm Download

Create Bootable Volume
References and Guidelines (NOTE)

Created as PVC on host kubernetes (pre-bounded)

VM: Windows VDI Server Setup
References and Guidelines (NOTE)

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 Server screen appears
  • Click Load Driver

  • virtio-win disk โ†’ amd64 โ†’ select latest drive

  • Select Red Hat VirtIO SCSI controller and then Install

  • Install on VirtIO disk, which can now be viewed easily

Network Setup
powershell
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 Start button > Go to Settings > System > **Remote Desktop**.
  • Change the toggle to โ€˜Onโ€™.
Service exposure/access
References and Guidelines (NOTE)

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
References and Guidelines (NOTE)

SSH default port: 22

Default access account: ubuntu

Mount disk
bash
# lvm์—†์ด ext4
sudo mkfs.ext4 /dev/vdb
sudo mkdir /workspace
sudo mount /dev/vdb /workspace
Network
Cluster Network
References and Guidelines (NOTE)

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 LoadBalancer settings 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
References and Guidelines (NOTE)

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
References and Guidelines (NOTE)

โ€“ 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
References and Guidelines (NOTE)

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
References and Guidelines (NOTE)

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-longhorn cannot be modified, so create a new SC

  • Number of Replica = 1

  • set Default

Enable Rancher & Longhorn UI
References and Guidelines (NOTE)

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
  1. Rancher

  2. 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

bash
# 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
References and Guidelines (NOTE)

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
yaml
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
References and Guidelines (NOTE)

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)
yaml
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)

bash
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
yaml
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
yaml
# ... (์•ž๋ถ€๋ถ„ ์ƒ๋žต)
    spec:
      # ...
      networks:
      - name: default
        # pod: {}  <-- ์ด ๋ถ€๋ถ„์„ ์ง€์šฐ๊ณ  ์•„๋ž˜ multus๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
        multus:
          networkName: harvester-public/vlan1 # <-- ์‹ค์ œ ์‚ฌ์šฉํ•˜๋ ค๋Š” ๋„คํŠธ์›Œํฌ ์ด๋ฆ„ (Namespace/Name)
      domain:
        # ...
        devices:
          interfaces:
          - bridge: {} # <-- masquerade์—์„œ bridge๋กœ ๋ณ€๊ฒฝ
            model: virtio
            name: default
# ... (๋’ท๋ถ€๋ถ„ ์ƒ๋žต)
Backup
bash
# 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

References and Guidelines (NOTE)

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

bash
# 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

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *