Skip to content

Split View: KubeVirt 소스코드 읽기 맵: 어디서부터 읽어야 전체 구조가 잡히는가

|

KubeVirt 소스코드 읽기 맵: 어디서부터 읽어야 전체 구조가 잡히는가

들어가며

KubeVirt는 코드 양도 많고 계층도 깊다. 처음 repo를 열면 virt-api, virt-controller, virt-handler, virt-launcher, pkg/network, staging이 한꺼번에 보여서 어디부터 읽어야 할지 막막해진다.

하지만 실제로는 읽는 순서만 잘 잡으면 구조가 꽤 선명해진다. 이 시리즈 마지막 글에서는 "KubeVirt를 이해하기 위해 어떤 질문을 어떤 패키지에서 해결해야 하는가"를 기준으로 소스 읽기 지도를 정리한다.

먼저 가져야 할 질문

KubeVirt 코드를 읽을 때는 파일 트리보다 질문 순서가 중요하다. 나는 아래 질문 순서를 추천한다.

  1. VM은 어떤 Kubernetes 객체로 표현되는가
  2. 그 객체를 누가 감시하고 Pod를 만드는가
  3. node에 도착한 뒤 누가 libvirt와 QEMU를 실행하는가
  4. Pod 네트워크가 guest 네트워크로 어떻게 바뀌는가
  5. migration은 control plane과 data plane에서 각각 어떻게 진행되는가
  6. 실제로 어떤 kernel primitive가 쓰이는가

이 질문 순서대로 읽으면, KubeVirt를 "엄청 큰 Go repo"가 아니라 "VM 실행 파이프라인"으로 볼 수 있다.

1단계: API 타입부터 읽는다

가장 먼저 읽을 곳은 staging/src/kubevirt.io/api/core/v1이다.

특히 다음 파일이 핵심이다.

  • types.go
  • schema.go

여기서 이해해야 할 것은 세 가지다.

  • VirtualMachine
  • VirtualMachineInstance
  • VirtualMachineInstanceMigration

이 레이어를 먼저 읽으면 이후 controller 코드에서 무엇을 reconcile하는지 보이기 시작한다. 특히 status, conditions, migrationState, interface 타입을 먼저 읽어 두면 뒤에서 나오는 거의 모든 로직이 쉬워진다.

즉 API 타입은 문서가 아니라, 나머지 코드를 해석하는 사전이다.

2단계: virt-api에서 사용자 의도가 어떻게 표준화되는지 본다

다음은 virt-api와 admission 계층이다. 여기서 봐야 할 질문은 이것이다.

"사용자가 넣은 spec가 어떤 검증과 defaulting을 거쳐 controller가 처리 가능한 객체가 되는가"

이 단계를 읽으면:

  • 어떤 필드 조합이 허용되는지
  • 어떤 subresource가 있는지
  • VMI가 바로 실행되는지, VM을 통해 관리되는지

가 잡힌다.

virt-api는 실행 엔진이 아니라, 사용자 선언을 안전한 control-plane 입력으로 바꾸는 문이다.

3단계: virt-controller/watch에서 Pod 생성과 handoff를 읽는다

그다음은 pkg/virt-controller/watch다. 이 단계에서 질문은 바뀐다.

"이 선언을 누가 launcher Pod와 migration Pod로 바꾸는가"

특히 다음 흐름이 중요하다.

  • VMI controller
  • migration controller
  • template service

여기서 이해해야 할 포인트는 다음이다.

  • informer와 work queue
  • owner reference와 expectation
  • launcher Pod manifest 렌더링
  • migration target Pod 생성

즉 이 구간은 KubeVirt가 Kubernetes controller 패턴을 가장 정석적으로 사용하는 층이다.

4단계: virt-handler를 읽으면 "node에서 실제로 누가 일하는가"가 보인다

많은 사람이 여기서 비로소 KubeVirt의 실체를 느낀다. pkg/virt-handler는 node-local agent이고, VM 운영의 상당 부분이 여기서 이뤄진다.

읽기 시작할 때는 다음 파일이 좋다.

  • vm.go
  • migration-target.go
  • migration-proxy/migration-proxy.go
  • seccomp/seccomp.go

이 레이어에서 답하는 질문은 이런 것들이다.

  • node에 있는 VMI와 domain을 어떻게 sync하는가
  • launcher와 어떤 RPC로 통신하는가
  • migration source와 target를 어떻게 준비하는가
  • cgroup, device, network, seccomp를 어떻게 만지는가

virt-handler는 KubeVirt의 "현장 반장"에 가깝다.

5단계: cmd/virt-launchervirtwrap에서 실제 VM 실행 경로를 읽는다

VM이 실제로 어떻게 뜨는지 보려면 결국 cmd/virt-launcher/virt-launcher.gopkg/virt-launcher/virtwrap로 내려가야 한다.

여기서 중요한 질문은 이것이다.

"VMI spec가 libvirt domain과 QEMU 실행 상태로 어떻게 변환되는가"

핵심 포인트는 다음이다.

  • libvirt 연결 생성
  • command server 시작
  • domain manager 구현
  • guest agent polling
  • event 수집
  • domain stats 수집

그리고 반드시 함께 봐야 할 패키지가 converter다. pkg/virt-launcher/virtwrap/converter/converter.go는 VMI spec를 domain XML 관점으로 번역하는 핵심 레이어다.

virtwrap은 KubeVirt에서 가장 하이퍼바이저에 가까운 코드다.

6단계: 네트워크는 pkg/network를 묶어서 읽는다

KubeVirt 네트워크는 파일 하나로 이해되지 않는다. 다음 패키지를 묶어서 읽는 편이 좋다.

  • setup
  • multus
  • controllers
  • dhcp
  • migration

질문은 단순하다.

"Pod가 이미 받은 네트워크를 guest가 어떻게 쓰게 되는가"

이때 봐야 할 것은:

  • Pod NIC 읽기
  • TAP와 bridge 준비
  • masquerade 주소 계산
  • DHCP 응답
  • Multus annotation 생성
  • interface status 반영

즉 KubeVirt 네트워크는 CNI를 대체하는 게 아니라, Pod network를 guest-visible network로 재가공하는 코드다.

7단계: migration은 controller와 launcher를 왕복하며 읽어야 한다

live migration은 한 파일에서 끝나지 않는다. 꼭 왕복해서 읽어야 한다.

  • control plane: pkg/virt-controller/watch/migration/migration.go
  • node plane: pkg/virt-handler/migration-target.go
  • transport 보조: pkg/virt-handler/migration-proxy/migration-proxy.go
  • hypervisor plane: pkg/virt-launcher/virtwrap/live-migration-source.go

읽을 때는 다음 질문을 붙이면 좋다.

  • target Pod는 누가 만들었는가
  • sync address와 port는 어디서 채워지는가
  • pre-copy에서 post-copy로 누가 전환하는가
  • abort와 timeout은 누가 판단하는가

즉 migration은 controller, node agent, launcher를 세로로 관통해서 읽어야 이해된다.

8단계: host 통합 코드는 kernel primitive 관점으로 읽는다

마지막에는 host 통합 코드를 다시 모아 읽는 것이 좋다.

  • cmd/virt-chroot
  • pkg/virt-handler/cgroup
  • pkg/virt-handler/selinux
  • pkg/network/driver/virtchroot

이때는 Kubernetes 추상화보다 Linux primitive를 기준으로 읽어야 한다.

  • chroot
  • namespace 진입
  • cgroup v1, v2
  • SELinux label 전환
  • TAP 생성
  • device 접근 허용

이 단계까지 보면 "왜 VM이 Pod 위에서 구현될 수 있었는가"라는 질문의 기술적 답이 거의 완성된다.

내가 추천하는 실제 읽기 순서

처음부터 끝까지 한 번 읽는다면 이 순서를 추천한다.

  1. staging/src/kubevirt.io/api/core/v1/types.go
  2. staging/src/kubevirt.io/api/core/v1/schema.go
  3. pkg/virt-controller/watch/vmi/vmi.go
  4. pkg/virt-controller/watch/migration/migration.go
  5. pkg/virt-handler/vm.go
  6. pkg/virt-handler/migration-target.go
  7. cmd/virt-launcher/virt-launcher.go
  8. pkg/virt-launcher/virtwrap/manager.go
  9. pkg/virt-launcher/virtwrap/converter/converter.go
  10. pkg/network/setup/network.go
  11. pkg/network/setup/podnic.go
  12. pkg/network/controllers/vmi.go
  13. pkg/network/multus/annotation.go
  14. pkg/virt-launcher/virtwrap/live-migration-source.go
  15. pkg/virt-handler/migration-proxy/migration-proxy.go
  16. pkg/virt-handler/seccomp/seccomp.go

이 순서는 control plane에서 시작해서 node, launcher, network, migration, kernel integration으로 내려가는 흐름이다.

읽을 때 특히 주의할 점

1. 패키지 단위보다 handoff 지점을 보라

KubeVirt는 handoff가 많은 시스템이다.

  • API에서 controller로
  • controller에서 launcher Pod로
  • controller에서 virt-handler로
  • virt-handler에서 launcher RPC로
  • launcher에서 libvirt와 QEMU로

따라서 패키지 경계보다 "누가 다음 단계로 넘기는가"를 추적하는 편이 이해가 빠르다.

2. status 필드 정의를 계속 다시 보라

코드를 읽다 길을 잃으면 types.go의 status 구조체로 다시 돌아오는 것이 좋다. 실제로 많은 controller 로직은 status를 채우거나 해석하는 코드다.

3. migration은 반드시 source와 target를 동시에 생각하라

단일 VM lifecycle에 익숙한 사람은 migration 코드를 읽다가 자주 헷갈린다. source Pod, target Pod, source node, target node, source state, target state가 동시에 존재하기 때문이다.

시리즈 전체를 한 문장으로 요약하면

KubeVirt는 Kubernetes 위에 또 하나의 하이퍼바이저를 올린 것이 아니라, Kubernetes의 선언형 control plane과 Linux의 가상화 primitive, 그리고 libvirt와 QEMU를 정교하게 이어 붙인 orchestrator다.

마무리

이 시리즈에서는 API 타입에서 시작해 controller, node agent, launcher, networking, migration, kernel, 보안, 관측성, 실패 모드까지 KubeVirt를 한 줄기로 따라가 봤다. 처음에는 복잡해 보여도, 실제로는 선언을 Pod로 바꾸고, Pod 안에서 QEMU를 띄우고, Linux primitive로 네트워크와 장치를 붙이며, migration과 상태 보고를 controller 패턴으로 감싸는 구조다.

결국 "VM이 어떻게 Pod 위에서 가능했는가"라는 질문의 답은 하나의 마법이 아니라, 여러 계층의 handoff를 정확히 이해하는 데 있다. KubeVirt 소스코드는 그 handoff를 가장 솔직하게 보여 주는 지도다.

KubeVirt Source Code Reading Map: Where to Start to Grasp the Entire Structure

Introduction

KubeVirt has a large codebase and deep layers. When you first open the repo, virt-api, virt-controller, virt-handler, virt-launcher, pkg/network, and staging all appear at once, making it hard to know where to start reading.

However, if you get the reading order right, the structure becomes quite clear. In this final post of the series, we organize a source code reading map based on "which questions should be answered by which packages to understand KubeVirt."

Questions to Start With

When reading KubeVirt code, the order of questions matters more than the file tree. I recommend the following order:

  1. What Kubernetes objects represent a VM?
  2. Who watches those objects and creates Pods?
  3. After arriving on a node, who runs libvirt and QEMU?
  4. How does the Pod network become the guest network?
  5. How does migration proceed in the control plane and data plane respectively?
  6. What kernel primitives are actually used?

Reading in this order lets you see KubeVirt not as "a huge Go repo" but as a "VM execution pipeline."

Step 1: Start with API Types

The first place to read is staging/src/kubevirt.io/api/core/v1.

These files in particular are key:

  • types.go
  • schema.go

Three things to understand here:

  • VirtualMachine
  • VirtualMachineInstance
  • VirtualMachineInstanceMigration

Reading this layer first makes it visible what controller code reconciles afterward. Especially reading status, conditions, migrationState, and interface types first makes almost all subsequent logic easier.

In other words, API types are not documentation -- they are the dictionary for interpreting the rest of the code.

Step 2: See How User Intent Is Standardized in virt-api

Next is virt-api and the admission layer. The question here is:

"What validation and defaulting does the user's spec go through to become an object the controller can process?"

Reading this step reveals:

  • Which field combinations are allowed
  • What subresources exist
  • Whether VMIs run directly or are managed through VMs

In other words, virt-api is not an execution engine -- it is the gate that transforms user declarations into safe control-plane inputs.

Step 3: Read Pod Creation and Handoff in virt-controller/watch

Next is pkg/virt-controller/watch. The question changes here:

"Who turns this declaration into launcher Pods and migration Pods?"

These flows in particular are important:

  • VMI controller
  • Migration controller
  • Template service

Key points to understand:

  • Informers and work queues
  • Owner references and expectations
  • Launcher Pod manifest rendering
  • Migration target Pod creation

This section is where KubeVirt uses the Kubernetes controller pattern most classically.

Step 4: Reading virt-handler Reveals "Who Actually Works on the Node"

Many people first feel the substance of KubeVirt here. pkg/virt-handler is the node-local agent, and a significant portion of VM operations happens here.

Good starting files:

  • vm.go
  • migration-target.go
  • migration-proxy/migration-proxy.go
  • seccomp/seccomp.go

Questions answered at this layer:

  • How does it sync VMIs and domains on the node?
  • What RPC does it use to communicate with the launcher?
  • How does it prepare migration source and target?
  • How does it handle cgroup, device, network, and seccomp?

In other words, virt-handler is closest to being KubeVirt's "on-site foreman."

Step 5: Read Actual VM Execution Path in cmd/virt-launcher and virtwrap

To see how a VM actually starts, you must descend to cmd/virt-launcher/virt-launcher.go and pkg/virt-launcher/virtwrap.

The important question here is:

"How is the VMI spec transformed into a libvirt domain and QEMU execution state?"

Key points:

  • libvirt connection creation
  • Command server startup
  • Domain manager implementation
  • Guest agent polling
  • Event collection
  • Domain stats collection

And a package that must be read alongside is converter. pkg/virt-launcher/virtwrap/converter/converter.go is the core layer that translates VMI spec into domain XML perspective.

In other words, virtwrap is the code closest to the hypervisor in KubeVirt.

Step 6: Read Networking by Bundling pkg/network

KubeVirt networking cannot be understood from a single file. It is better to read these packages together:

  • setup
  • multus
  • controllers
  • dhcp
  • migration

The question is simple:

"How does the guest use the network the Pod already received?"

What to look at:

  • Pod NIC reading
  • TAP and bridge preparation
  • Masquerade address calculation
  • DHCP responses
  • Multus annotation generation
  • Interface status reflection

In other words, KubeVirt networking does not replace CNI -- it is code that reprocesses Pod network into guest-visible network.

Step 7: Migration Must Be Read by Going Back and Forth Between Controller and Launcher

Live migration does not end in a single file. You must read it by going back and forth:

  • Control plane: pkg/virt-controller/watch/migration/migration.go
  • Node plane: pkg/virt-handler/migration-target.go
  • Transport support: pkg/virt-handler/migration-proxy/migration-proxy.go
  • Hypervisor plane: pkg/virt-launcher/virtwrap/live-migration-source.go

Good questions to attach while reading:

  • Who created the target Pod?
  • Where are sync address and port filled in?
  • Who triggers the transition from pre-copy to post-copy?
  • Who decides abort and timeout?

In other words, migration must be read vertically through controller, node agent, and launcher.

Step 8: Read Host Integration Code from a Kernel Primitive Perspective

Finally, it is good to re-read the host integration code together.

  • cmd/virt-chroot
  • pkg/virt-handler/cgroup
  • pkg/virt-handler/selinux
  • pkg/network/driver/virtchroot

At this point, read based on Linux primitives rather than Kubernetes abstractions.

  • chroot
  • Namespace entry
  • cgroup v1, v2
  • SELinux label switching
  • TAP creation
  • Device access allowance

After reaching this step, the technical answer to "why VMs could be implemented on top of Pods" is nearly complete.

If reading from start to finish once, I recommend this order:

  1. staging/src/kubevirt.io/api/core/v1/types.go
  2. staging/src/kubevirt.io/api/core/v1/schema.go
  3. pkg/virt-controller/watch/vmi/vmi.go
  4. pkg/virt-controller/watch/migration/migration.go
  5. pkg/virt-handler/vm.go
  6. pkg/virt-handler/migration-target.go
  7. cmd/virt-launcher/virt-launcher.go
  8. pkg/virt-launcher/virtwrap/manager.go
  9. pkg/virt-launcher/virtwrap/converter/converter.go
  10. pkg/network/setup/network.go
  11. pkg/network/setup/podnic.go
  12. pkg/network/controllers/vmi.go
  13. pkg/network/multus/annotation.go
  14. pkg/virt-launcher/virtwrap/live-migration-source.go
  15. pkg/virt-handler/migration-proxy/migration-proxy.go
  16. pkg/virt-handler/seccomp/seccomp.go

This order flows from the control plane down through node, launcher, network, migration, and kernel integration.

Special Notes While Reading

1. Look at Handoff Points Rather Than Package Units

KubeVirt is a system with many handoffs.

  • From API to controller
  • From controller to launcher Pod
  • From controller to virt-handler
  • From virt-handler to launcher RPC
  • From launcher to libvirt and QEMU

Therefore, tracking "who hands off to the next stage" is faster for understanding than following package boundaries.

2. Keep Revisiting Status Field Definitions

When lost while reading code, returning to the status structs in types.go is helpful. In practice, much of the controller logic is code that fills or interprets status.

3. Always Think About Source and Target Simultaneously for Migration

People accustomed to single VM lifecycles often get confused reading migration code. This is because source Pod, target Pod, source node, target node, source state, and target state all exist simultaneously.

Summarizing the Entire Series in One Sentence

KubeVirt is not another hypervisor placed on top of Kubernetes -- it is an orchestrator that precisely stitches together Kubernetes' declarative control plane, Linux's virtualization primitives, and libvirt with QEMU.

Conclusion

In this series, we followed KubeVirt in a single thread from API types through controller, node agent, launcher, networking, migration, kernel, security, observability, and failure modes. While it looks complex at first, it is actually a structure of transforming declarations into Pods, starting QEMU inside Pods, attaching networks and devices with Linux primitives, and wrapping migration and status reporting in controller patterns.

Ultimately, the answer to "how were VMs possible on top of Pods" is not one piece of magic but lies in precisely understanding the handoffs across multiple layers. The KubeVirt source code is the map that most honestly shows those handoffs.