Kubernetes + UIPath Window - 2
kubevirt 사용한 Window Snapshost 가상머신
by HOON
1
Last updated on April 8, 2025, 1:50 p.m.
안녕하세요, 오늘은 저번 포스트에 이어서 window를 kubevirt 가상머신으로 생성해서 올려볼거에요.
저번 포스트에서는 Custom Window ISO를 생성해서, 해당 이미지를 cd-rom으로 인식 하게 한 후
kubevirt에서 가상머신을 생성해줬습니다.
다만 이 방식은 윈도우를 처음부터 생성하다보니 다소 비효율적이라 생각했어요.
그래서 생각한게 window snapshot 자체를 이미지로 생성해서 이미 window가 생성된 상태로 가상머신을 생성하면 어떨까 생각했습니다. 대부분의 작업은 Custom Window ISO를 생성할때랑 비슷해요.
한번 보고 오시면 이해가 빠를거에요!
Custom Window ISO로 kubevirt 가상머신 띄우기
자 그럼, 저희가 필요한 과정은 다음과 같습니다.
- window가 내장된 snapshot을 생성하고, kubevirt가 인식 할 수 있도록 qcow2로 변경하기
- qcow2로 가상머신을 생성 할 수 있도록 리소스 생성하기
- 테스트하기
Snapshot, qcow2 생성하기
먼저, window가 내장된 snapshot을 생성하겠습니다. 과정은 snapshot → vdi → raw → qcow2로 파일이 변환되어야합니다.
vdi→raw로 변환하는게 혹시라도 누락될 mbr을 예방 할 수 있다해서 raw파일 과정을 한번 거치겠습니다.
#윈도우가 설치되어있는 vm("window-test2")의 snapshot을 찍습니다.
$ VBoxManage clonevm "window-test2" \ 1 ✘ took 1m 18s
--snapshot "clean-uipath" \
--mode all \
--name "WinUiPath-export" \
--register
0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100%
Machine has been successfully cloned as "WinUiPath-export"
#실제 생성한 vdi로 window가 정상 부팅되는지 확인하기위해 vm을 띄워보겠습니다.
$ VBoxManage startvm "WinUiPath-export" --type gui ✔ took 1m 44s
Waiting for VM "WinUiPath-export" to power on...
VM "WinUiPath-export" has been successfully started.
#vdi에 이상이 없다면, poweroff 하여 스냅샷 병합을 준비할게요.
$ VBoxManage controlvm "WinUiPath-export" poweroff ✔ took 13s
0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100%
#저희는 WinUiPath-export 라는 이름으로 스냅샷을 생성하려했으나, 아래처럼 vdi는 나오지만 원하는 이름이 아니에요.
#virtualbox에서 스냅샷을 찍는다해도, 루트 디스크, 즉 window가 설치된 모든 디스크의 내용을 찍은게 아니기때문에 병합 과정이 필요해요.
$ VBoxManage showvminfo "WinUiPath-export" | grep -i '\.vdi' ✔
Location: "/Users/hoon/VirtualBox VMs/WinUiPath-export/Snapshots/{d2f04478-f79b-4175-b43f-515d1fc89edb}.vdi"
----------------
#아래 3단계를 실행해서 완벽한 루트 스냅샷으로 생성하겠습니다.
# 1) 스냅샷 복원 ✔
VBoxManage snapshot "window-test2" restore "clean-uipath"
VBoxManage controlvm "window-test2" poweroff
# 2) base VDI 경로 확인
VBoxManage showvminfo "window-test2" | grep -i '\.vdi'
# 3) base VDI → 새 VDI 로 ‘클론’ (스냅샷 자동 병합)
VBoxManage clonemedium disk \
"/Users/hoon/VirtualBox VMs/window-test2/window-test2.vdi" \
"/Volumes/BigDisk/WinUiPath-merged.vdi" \
--format VDI
#그럼 지정한 폴더에 (여기에서는 "/Volumes/BigDisk/WinUiPath-merged.vdi")에 vdi 파일이 생성될거에요.
이제 vdi가 생성됐으니 vdi→raw 파일 과정을 진행해볼게요.
# 저희가 생성한 루트 스냅샷을 raw 파일로 변환합니다.
$ qemu-img convert -f vdi -O raw WinUiPath-merged.vdi WinUiPath.raw
# 정상적으로 WinUiPath.raw가 생성됐습니다!
$ ls ✔ took 1m 40s
Logs WinUiPath-merged.vdi window-test2.vbox window-test2.vdi
Snapshots WinUiPath.raw window-test2.vbox-prev
raw파일 생성을 확인했다면, 이제 raw→qcow2 과정을 보겠습니다.
# raw 파일을 qcow2로 변환할게요.
$ qemu-img convert -f raw -O qcow2 WinUiPath.raw WinUiPath.qcow2
# 변환이 다 됐다면, 한번 확인해보겠습니다. virtual size가 대략 40~50GiB면 적당합니다.
$ qemu-img info WinUiPath.qcow2 ✔ took 2m 20s
image: WinUiPath.qcow2
file format: qcow2
virtual size: 50 GiB (53687091200 bytes)
disk size: 19.7 GiB
cluster_size: 65536
Format specific information:
compat: 1.1
compression type: zlib
lazy refcounts: false
refcount bits: 16
corrupt: false
extended l2: false
Child node '/file':
filename: WinUiPath.qcow2
protocol type: file
file length: 19.7 GiB (21179400192 bytes)
disk size: 19.7 GiB
# 생성된 qcow2 파일을 k8s 클러스터로 이동시키겠습니다.
$ scp -P 2222 WinUiPath.qcow2 k8s-master@127.0.0.1:/home/k8s-master ✔
k8s-master@127.0.0.1's password:
WinUiPath.qcow2 100% 20GB 27.8MB/s 12:05
qcow2까지 생성되고, 생성된 이미지에 이상이없다면, scp를 통해 k8s 클러스터로 파일을 옮깁니다.
이러면 일단 이미지 준비는 끝났습니다.
qcow2 기반 가상머신 생성
생성된 이미지로 Kubevirt를 통해 가상머신을 띄워보겠습니다.
일단 custom window와 마찬가지로, DV(Data Volume) 생성 → image upload → virtual machine 생성 순서로 진행할게요.
우선 저희가 생성한 qcow2 이미지가 실제로 부팅가능한 윈도우 파일인지 확인해볼게요. 이 과정은 확인을 위해 진행했어요. 필요없으면 넘어가도 괜찮습니다!
다만 저는 Boot에 * 표시가 없던 상태로 vm을 띄우니 no bootable disk가 발생해서 이미지에 문제가 있다는걸 알았습니다. Boot에 * 표시가 있다면 문제없습니다!
k8s-master@k8s-master:~$ sudo modprobe nbd max_part=8
[sudo] password for k8s-master:
k8s-master@k8s-master:~$ sudo qemu-nbd -f qcow2 -c /dev/nbd0 WinUiPath.qcow2
k8s-master@k8s-master:~$ sudo fdisk -l /dev/nbd0
Disk /dev/nbd0: 50 GiB, 53687091200 bytes, 104857600 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x6cc1fa2b
Device Boot Start End Sectors Size Id Type
/dev/nbd0p1 * 2048 104447 102400 50M 7 HPFS/NTFS/exFAT
/dev/nbd0p2 104448 103728239 103623792 49.4G 7 HPFS/NTFS/exFAT
/dev/nbd0p3 103729152 104853503 1124352 549M 27 Hidden NTFS WinRE
우선, StorageClass 를 먼저 볼게요. 저는 sc를 2가지 사용하고있었습니다.
k8s-master@k8s-master:~/window$ k get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path rancher.io/local-path Delete WaitForFirstConsumer false 6d2h
local-path-immediate rancher.io/local-path Delete Immediate false 6d2h
보시다시피 하나는 WaitForFirstConsumer 나머지 하나는 Immediate 입니다.
각각의 작동방식은 간단하게 pvc가 생성되면 실제로 해당 pvc바인딩을 잡는 pod가 생성되면 pv를 바인딩하고 Immediate는 pod 상관없이 바로 pv를 바인딩하려합니다.
이 설명을 왜 드리냐면, dv 생성시에 그 차이가 분명하게 나오기 때문이에요. (2시간 삽질 결과입니다ㅠ)
dv를 생성해볼까요?
k8s-master@k8s-master:~/window$ cat new-windows-dv.yaml
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
name: win-uipath-dv
namespace: kubevirt
annotations:
volume.kubernetes.io/selected-node: "k8s-node1"
cdi.kubevirt.io/storage.upload.target: "true"
spec:
source:
upload: {}
contentType: kubevirt
pvc:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 52Gi # (virtual size 이상으로)
storageClassName: local-path # 지금은 WaitForFirstCounsumer으로 지정했습니다.
이런식으로 지정해주면 문제없이 Bound가 될거에요
k8s-master@k8s-master:~$ k get dv
NAME PHASE PROGRESS RESTARTS AGE
win-uipath-dv UploadScheduled N/A 10s
k8s-master@k8s-master:~$ k get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
win-uipath-dv Bound pvc-04d3148d-1e6f-494c-b21e-f00c693b9c6c 52Gi RWO local-path <unset> 11s
win-uipath-dv-scratch Bound pvc-7f6139a2-86b9-4fe6-b81a-58fe5758391c 55834574848 RWO local-path <unset> 8s
여기서 주의해야할점은 win-uipath-dv-scratch는 저희가 지정하지않았습니다. 얘는 뭘까요?
scratch의 역할은 업로드하는 이미지 데이터를 임시저장, 프로세스 중 필요한 작업(캐싱,변환,검증)을 수행하는 작업공간입니다. 즉 지금은 dv를 생성만 하고, 아직 이미지를 업로드하지 않았기때문에 대기상태로 남아있습니다. 이미지가 성공적으로 업로드되면 scratch는 사라집니다.
잠깐 길을 벗어났는데, 다시 sc를 설명드리자면 제가 설명드린 scratch가 immediate sc를 기반으로 생성된다면 Pending 상태가 됩니다. (아래 이벤트 참조)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 31s default-scheduler 0/2 nodes are available: persistentvolumeclaim "win-uipath-dv-scratch" not found. preemption: 0/2 nodes are available: 2 Preemption is not helpful for scheduling.
Warning FailedScheduling 29s default-scheduler 0/2 nodes are available: pod has unbound immediate PersistentVolumeClaims. preemption: 0/2 nodes are available: 2 Preemption is not helpful for scheduling.
win-uipath-dv는 저희가 생성시점에 k8s-node1 이라고 지정을 해줬습니다. 다만 scratch는 따로 지정받지않아서 생성 시점에 어느 node에 생성되어야할지 잡지 못하는거에요. 따라서 Immediate가 아닌 WaitForFirstConsumer를 사용하면 됩니다!!
자 이제 dv도 생성 되었으니, 이미지를 업로드 해볼게요. Custom Window ISO 때와 비슷합니다.
port-forward를 사용해서 프록시 서버를 잡아주고, 업로드를 해볼게요.
#terminal 1
k8s-master@k8s-master:~$ubectl port-forward -n cdi svc/cdi-uploadproxy 8443:443
Forwarding from 127.0.0.1:8443 -> 8443
Forwarding from [::1]:8443 -> 8443
# terminal 2
k8s-master@k8s-master:~/window$ virtctl image-upload dv win-uipath-dv \
> --size=52Gi \
> --uploadproxy-url=https://127.0.0.1:8443
> --image-path=/home/k8s-master/window/WinUiPath.qcow2 \
> --insecure
Using existing PVC kubevirt/win-uipath-dv
Uploading data to https://127.0.0.1:8443
19.72 GiB / 19.72 GiB [--------------------------------------------] 100.00% 33.57 MiB p/s 10m2s
Uploading data completed successfully, waiting for processing to complete, you can hit ctrl-c without interrupting the progress
Processing completed successfully
Uploading /home/k8s-master/window/WinUiPath.qcow2 completed successfully
성공적으로 업로드가 됐습니다. dv를 확인해야겠죠??
k8s-master@k8s-master:~/window$ k get dv
dNAME PHASE PROGRESS RESTARTS AGE
win-uipath-dv Succeeded N/A 51m
k8s-master@k8s-master:~/window$ k get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
win-uipath-dv Bound pvc-04d3148d-1e6f-494c-b21e-f00c693b9c6c 52Gi RWO local-path <unset> 51m
Succeeded 상태로 변경됐고, 작업을 위해 생성됐던 scratch도 사라진걸 확인 할 수 있습니다.
그럼 이제 virtualmachine 을 생성할게요. 메니페스트 파일은 다음과 같습니다.
k8s-master@k8s-master:~/window$ cat new-vm-windows.yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: win-uipath
spec:
runStrategy: RerunOnFailure
template:
metadata:
labels: { kubevirt.io/domain: win-uipath }
spec:
domain:
cpu: { cores: 2 }
resources: { requests: { memory: 4Gi } }
machine: { type: q35 }
firmware:
bootloader:
bios: {} # ← MBR 디스크이므로 BIOS
devices:
disks:
- name: os
disk: { bus: sata }
bootOrder: 1
interfaces:
- name: default
masquerade: {}
networks:
- name: default
pod: {}
volumes:
- name: os
dataVolume:
name: win-uipath-dv
이전 Custom Window ISO와의 차이점은 크게 없고, disk만 다르네요.
![]() |
![]() |
---|---|
Custom Window ISO | Snapshot |
이제 마지막으로 k apply -f new-vm-windows.yaml으로 가상 머신을 생성 후 조금만 기다려주면..
k8s-master@k8s-master:~/window$ k get virtualmachine
NAME AGE STATUS READY
win-uipath 10s Running True
정상적으로 실행됩니다!
VNC를 통해 접속해서 확인해보면??
# Terminal 1 / 포트포워딩
k8s-master@k8s-master:~/window$ virtctl vnc win-uipath --proxy-only --address=0.0.0.0 --port 30050
{"port":30050}
{"component":"portforward","level":"info","msg":"connection timeout: 1m0s","pos":"vnc.go:149","timestamp":"2025-04-08T03:27:20.458600Z"}
# Terminal 2 / SSH 터널링
$ ssh -L 30050:localhost:30050 -p 2222 k8s-master@127.0.0.1
k8s-master@127.0.0.1's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-212-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
성공적으로 윈도우가 부팅되고있습니다.
쪼금만 기다려보면…
아래와 같이 저희가 찍은 Window+UIPath 가 설치 된 상태로 바로 부팅 할 수 있습니다!!
Leave a Comment: