들어가며
이제 KubeVirt의 가장 중요한 실행 지점을 보자. `virt-controller`는 Pod를 만들고, `virt-handler`는 노드에서 조율한다. 그렇다면 실제 VM은 어디서 뜰까? 답은 `virt-launcher` Pod 안이다.
정확히 말하면, `virt-launcher` Pod는 VM 하나를 위한 실행 셸이고, 그 안에서 libvirt와 QEMU가 guest를 실제로 구동한다. `cmd/virt-launcher/virt-launcher.go`와 `pkg/virt-launcher/virtwrap/manager.go`는 이 실행 계층의 중심이다.
`virt-launcher` Pod를 어떻게 이해해야 하나
많은 초보 설명은 "`virt-launcher`가 VM을 띄운다"라고 끝난다. 하지만 더 정확한 설명은 이렇다.
- `virt-launcher` Pod는 VM 실행에 필요한 namespace와 cgroup 경계를 제공한다.
- 그 Pod 내부의 `virt-launcher` 프로세스는 command server와 domain manager를 준비한다.
- libvirt 연결이 생성된다.
- QEMU domain이 정의되고 실행된다.
- 이후 이벤트와 guest agent, stats가 계속 감시된다.
즉 `virt-launcher`는 단순 container entrypoint가 아니라 **VM 실행 제어 plane의 local runtime**이다.
`cmd/virt-launcher/virt-launcher.go`가 하는 일
이 엔트리 포인트 파일을 보면 초기화 순서가 꽤 명확하다.
1. libvirt event 구현 등록
프로세스 시작 시 `libvirt.EventRegisterDefaultImpl()`가 호출된다. 이는 이후 domain event monitoring을 위해 필요하다.
2. 각종 디렉터리 초기화
`initializeDirs`는 cloud-init, ignition, container disk, hotplug disk, secret, config map, downward metrics 등 guest 런타임에 필요한 여러 디렉터리를 준비한다.
여기서 이미 중요한 사실이 보인다. VM 부팅은 단순히 QEMU 바이너리 하나 띄우는 일이 아니라, **여러 디스크 소스와 메타데이터 채널을 정리하는 준비 작업**과 함께 간다.
3. libvirt 연결 생성
`createLibvirtConnection`은 기본적으로 `qemu:///system`에 연결하고, 비 root 모드에서는 session URI를 사용한다. 즉 `virt-launcher`는 단순히 libvirt 라이브러리를 링크하는 것이 아니라, **실제 libvirt 제어 plane에 client로 붙어 domain을 정의**한다.
4. command server 시작
`startCmdServer`는 unix socket 기반 command server를 띄운다. `virt-handler`는 이 소켓을 통해 launcher 측 domain manager를 호출한다.
5. domain event monitoring 시작
`startDomainEventMonitoring`은 libvirt 이벤트 루프를 돌리고 notifier를 시작한다. guest 상태 변화, 종료, agent 연동 상태가 여기서 반영된다.
왜 command server가 필요한가
이건 KubeVirt 구조를 이해하는 핵심 포인트다. `virt-handler`는 node agent이지만, QEMU는 launcher Pod 내부에 있다. 두 프로세스는 서로 다른 격리 경계에 있으므로 직접 함수 호출을 할 수 없다. 그래서 KubeVirt는 command server와 client 계층을 둔다.
흐름은 대략 이렇다.
1. `virt-handler`가 VMI를 reconcile한다.
2. launcher Pod 내부 socket에 요청을 보낸다.
3. command server가 `DomainManager` 메서드를 호출한다.
4. `LibvirtDomainManager`가 libvirt API를 통해 domain을 define, start, pause, migrate한다.
이 구조 덕분에 cluster node 쪽 orchestration과 Pod 내부 하이퍼바이저 제어가 분리된다.
`DomainManager` 인터페이스가 보여주는 책임 범위
`pkg/virt-launcher/virtwrap/manager.go`의 `DomainManager` 인터페이스를 보면, launcher가 단순 boot만 담당하는 것이 아니라는 점이 선명하다.
대표 메서드는 다음과 같다.
- `SyncVMI`
- `PauseVMI`
- `UnpauseVMI`
- `KillVMI`
- `DeleteVMI`
- `MigrateVMI`
- `PrepareMigrationTarget`
- `FinalizeVirtualMachineMigration`
- `HotplugHostDevices`
- `GetDomainStats`
- `GuestPing`
즉 launcher는 guest lifecycle 전체를 담당하는 local hypervisor adapter다.
libvirt가 하는 일, QEMU가 하는 일
이 둘은 자주 한 덩어리로 말해지지만 역할이 다르다.
libvirt
- domain 정의와 관리 API 제공
- XML 기반 domain model 유지
- migration API 제공
- stats와 event 수집 인터페이스 제공
QEMU
- 실제 guest CPU, 메모리, 디스크, NIC 에뮬레이션
- KVM과 결합한 실행 엔진
- virtio 장치 구현
KubeVirt는 대체로 libvirt를 주 제어 인터페이스로 사용하고, libvirt가 내부적으로 QEMU를 관리하도록 한다.
readiness와 socket 전환이 의미하는 것
`markReady`는 uninitialized socket 이름을 실제 socket 이름으로 바꾼다. 이 작은 동작이 중요한 이유는, `virt-handler`가 launcher가 아직 초기화 중인지 아니면 실제 명령을 받을 준비가 되었는지 구분할 수 있게 해주기 때문이다.
즉 launcher는 Pod가 Running이라고 곧바로 "VM 제어 준비 완료"라고 가정하지 않는다. **command plane readiness**가 별도로 있다.
guest agent와 이벤트 루프까지 launcher에 있는 이유
guest 내부 정보를 다루는 책임도 launcher에 많다. agent poller, guest info, filesystem, user, time sync 같은 기능이 `manager.go` 인터페이스와 `virt-launcher.go` 초기화 경로에 보인다.
이건 자연스럽다. guest agent는 결국 libvirt 또는 QEMU가 노출하는 guest 통신 채널과 가장 가깝기 때문이다. node agent인 `virt-handler`는 이를 직접 다루기보다 launcher에게 묻는 편이 맞다.
종료 시 왜 Pod가 바로 죽지 않을 수 있는가
`docs/components.md`도 언급하지만, Kubernetes가 `virt-launcher` Pod 종료를 시도할 때 guest가 아직 종료되지 않았다면 launcher는 signal을 guest 쪽으로 전달하고, 가능한 한 정상 종료를 기다린다.
이 동작은 매우 중요하다. 그렇지 않으면 Pod 종료가 guest 강제 종료와 거의 동일해져서 데이터 손상 위험이 커진다. KubeVirt는 Pod lifecycle과 VM lifecycle이 완전히 같은 것이 아니라는 점을 이 지점에서 드러낸다.
자주 하는 오해
오해 1: `virt-launcher`는 그냥 sidecar 없는 일반 컨테이너다
아니다. 이 Pod는 VM 실행 경계이며, 내부에 libvirt event loop, command server, guest agent 관련 로직이 있다.
오해 2: QEMU는 `virt-handler`가 직접 띄운다
아니다. `virt-handler`는 launcher의 command server를 호출하고, 실제 domain define과 실행은 launcher 내부 domain manager가 libvirt를 통해 수행한다.
오해 3: Pod Running이면 guest도 이미 정상이다
그렇지 않다. launcher 준비, libvirt 연결, domain define, agent 초기화는 별도 단계다.
디버깅 힌트
- launcher Pod는 살아 있는데 guest가 안 뜨면 command server readiness와 libvirt 연결을 본다.
- guest agent 관련 정보가 비면 launcher 쪽 poller와 event notifier를 본다.
- 종료가 오래 걸리면 guest shutdown 전달과 grace period를 함께 본다.
- migration 이상이면 `PrepareMigrationTarget`, `MigrateVMI`, `FinalizeVirtualMachineMigration` 경로를 분리해서 본다.
마무리
`virt-launcher`는 KubeVirt에서 가장 실행에 가까운 레이어다. 이 Pod는 VM을 위한 격리 경계를 제공하고, 그 내부의 launcher 프로세스는 libvirt 연결, command server, event monitor, guest agent poller를 세팅한다. 그 위에서 `LibvirtDomainManager`가 QEMU domain lifecycle을 제어한다. 결국 "VM이 Pod 위에서 돈다"는 말의 가장 구체적인 실체는 바로 이 launcher Pod 내부 실행 구조다.
다음 글에서는 launcher가 받은 VMI spec가 어떤 변환 과정을 거쳐 libvirt domain XML과 실제 guest 장치 구성으로 바뀌는지 보겠다.
현재 단락 (1/61)
이제 KubeVirt의 가장 중요한 실행 지점을 보자. `virt-controller`는 Pod를 만들고, `virt-handler`는 노드에서 조율한다. 그렇다면 실제 VM은 어...