Skip to content

Split View: virt-controller deep dive: informer, queue, reconcile로 VM Pod를 만드는 법

|

virt-controller deep dive: informer, queue, reconcile로 VM Pod를 만드는 법

들어가며

KubeVirt에서 "cluster-wide brain"에 가장 가까운 컴포넌트는 virt-controller다. VM을 실제로 실행하는 것은 노드 쪽의 virt-handlervirt-launcher지만, 어떤 Pod를 언제 만들고 어떤 상태를 다음 단계로 넘길지 조율하는 중심virt-controller다.

이번 글은 pkg/virt-controller/watch/application.gopkg/virt-controller/watch/vmi/vmi.go, pkg/virt-controller/watch/migration/migration.go를 중심으로 이 controller가 어떤 informer를 연결하고, 어떤 큐를 돌리며, 어떻게 launcher Pod를 생성하는지 본다.

virt-controller는 하나의 controller가 아니다

이름만 보면 단일 프로세스와 단일 reconcile loop처럼 느껴지지만, 실제로는 여러 watcher와 controller가 묶여 있다. application.go를 보면 다음 계열이 한곳에 배치된다.

  • VMI controller
  • VM controller
  • migration controller
  • replica set, pool, clone controller
  • snapshot, export, backup 관련 controller
  • node, workload update, disruption budget 관련 controller

virt-controller는 KubeVirt의 cluster-wide orchestration 집합이다.

왜 informer가 এত 많을까

NewController 시그니처를 보면 VMI controller만 해도 여러 informer를 받는다.

  • VMI informer
  • VM informer
  • Pod informer
  • PVC informer
  • migration informer
  • storage class informer
  • DataVolume, CDI informer
  • KubeVirt CR informer

이렇게 많은 이유는 VMI의 desired state가 VMI spec 하나만으로 결정되지 않기 때문이다.

  • 스토리지가 준비되었는가
  • 이미 Pod가 있는가
  • migration이 진행 중인가
  • cluster config가 어떤 feature gate를 켰는가
  • 네트워크 annotation을 어떻게 생성해야 하는가

controller는 이 모든 주변 상태를 종합해야 launcher Pod를 정확히 만들 수 있다.

watch/vmi/vmi.go가 보여주는 전형적인 패턴

VMI controller는 Kubernetes 표준 controller 패턴과 매우 닮아 있다.

1. 이벤트를 받는다

VMI, Pod, DataVolume, PVC, VM, KubeVirt 오브젝트에 event handler를 등록한다.

2. 키를 큐에 넣는다

실제 작업은 event handler에서 바로 하지 않고 queue로 넘긴다.

3. sync loop가 현재 상태를 읽는다

indexer와 store에서 VMI, Pod, PVC, migration 상태를 다시 읽어 desired state와 current state를 비교한다.

4. 필요한 Pod 또는 status를 만든다

launcher Pod 생성, annotation 업데이트, network status 업데이트, expectation 관리가 여기서 이뤄진다.

이 패턴의 장점은 race condition을 완화하고 idempotent한 reconcile을 가능하게 만든다는 점이다.

launcher Pod는 어떻게 만들어지는가

VMI controller는 직접 Pod YAML을 손으로 조립하지 않는다. templateService를 통해 RenderLaunchManifest 같은 메서드를 호출한다. 즉 controller는 "Pod가 필요하다"는 결정을 하고, template service가 실제 launcher Pod spec를 렌더링한다.

이 분리는 꽤 중요하다.

  • controller는 상태 판단에 집중한다
  • template service는 Pod spec 구성에 집중한다

덕분에 migration Pod, hotplug attachment Pod 같은 변형도 별도 rendering path로 처리할 수 있다.

controller가 network와 storage를 직접 아는 이유

많은 사람이 여기서 묻는다. "네트워크와 스토리지는 node에서 붙는 거 아닌가? 왜 controller가 annotation generator를 들고 있지?"

정답은 사전 배선 작업 때문이다.

watch/vmi/vmi.go를 보면 다음 의존성이 있다.

  • network annotations generator
  • storage annotations generator
  • network status updater
  • network spec validator
  • migration evaluator

즉 controller는 단순히 Pod를 만들기만 하는 것이 아니라, Pod가 노드에 도착했을 때 KubeVirt와 CNI 쪽이 필요한 컨텍스트를 알 수 있도록 annotation과 상태를 준비한다.

expectation 패턴이 중요한 이유

Kubernetes controller를 읽다 보면 expectations 패턴이 자주 나온다. KubeVirt도 마찬가지다.

VMI controller에는 다음이 있다.

  • pod expectations
  • vmi expectations
  • pvc expectations

이 패턴은 "내가 방금 Pod 생성 요청을 냈으니 informer 캐시가 아직 반영되지 않았더라도 너무 일찍 재시도하지 말자"는 문제를 해결한다. controller가 create call 직후 stale cache를 읽어서 중복 생성하는 일을 줄여준다.

대규모 클러스터에서 이 패턴은 매우 중요하다. VMI 하나가 일반 Pod보다 더 복잡한 주변 리소스를 갖기 때문에 중복 생성은 훨씬 더 큰 혼란을 만든다.

언제 virt-handler로 책임이 넘어가는가

docs/components.md는 Pod가 노드에 스케줄되어 nodeName이 정해진 뒤 책임이 virt-handler 쪽으로 넘어간다고 설명한다. 실제 mental model도 거의 이렇다.

  1. virt-controller가 launcher Pod를 만든다.
  2. Kubernetes가 Pod를 특정 node에 스케줄링한다.
  3. controller는 Pod와 VMI 상태를 반영한다.
  4. 해당 노드의 virt-handler가 그 VMI를 보고 VM 런치를 이어받는다.

virt-controller는 cluster scheduler와 node agent 사이의 handoff coordinator다.

migration에서는 왜 Pod가 둘일 수 있는가

이 점이 일반 Pod controller와 KubeVirt controller를 구분 짓는다. VMI는 평상시 "VMI 하나에 launcher Pod 하나"처럼 보이지만, migration 중에는 target Pod가 추가로 생긴다.

그래서 migration controller 쪽에서는:

  • target Pod 생성
  • source와 target 상태 추적
  • concurrent migration 제한
  • unschedulable timeout 관리

같은 로직이 추가된다.

pkg/virt-controller/watch/migration/migration.go를 보면 pending timeout, priority queue, policy store, handoff map 같은 장치가 등장한다. 이건 migration이 단순 Pod 재스케줄이 아니라, 살아 있는 guest를 이동하는 복합 작업이기 때문이다.

application.go가 보여주는 전체 오케스트레이션 그림

VirtControllerApp 구조체를 보면 KubeVirt가 cluster-wide에서 무엇을 관리하는지 감이 온다.

  • informer factory
  • 각종 resource informer
  • template service
  • cluster config
  • migration controller
  • backup, export, snapshot controller

여기서 알 수 있는 중요한 사실은 virt-controller가 단순 VM Pod 생성기보다 훨씬 넓은 역할을 가진다는 점이다. VM lifecycle 전반의 중앙 orchestration 계층이다.

자주 하는 오해

오해 1: virt-controller가 VM을 직접 실행한다

아니다. virt-controller는 Pod를 만들고 상태를 조율한다. 실제 VM 프로세스 런치는 node 쪽에서 일어난다.

오해 2: controller는 VMI만 보면 충분하다

아니다. Pod, PVC, migration, cluster config, CDI 상태를 함께 봐야 launcher Pod를 정확히 만들 수 있다.

오해 3: migration은 그냥 Pod 하나 다시 스케줄하는 일이다

아니다. 살아 있는 guest state와 네트워크, 디스크 가시성을 유지하면서 target Pod를 준비해야 하므로 별도의 migration controller가 필요하다.

운영자가 봐야 할 증상과 위치

VMI는 있는데 launcher Pod가 안 생긴다

  • virt-controller 로그
  • DataVolume 또는 PVC 준비 상태
  • network spec validation 오류

migration이 pending에서 오래 멈춘다

  • target Pod scheduling 가능 여부
  • migration controller timeout
  • cluster-wide parallel migration 제한

Pod는 있는데 상태가 어긋난다

  • informer 캐시 반영 지연
  • expectation 관련 재시도
  • VMI status patch 충돌

마무리

virt-controller는 KubeVirt의 cluster-wide 조율자다. 이 컴포넌트는 다양한 informer를 통해 VMI 주변 세계를 읽고, queue 기반 reconcile로 launcher Pod를 만들며, 적절한 시점에 책임을 virt-handler로 넘긴다. migration이 복잡한 이유도 여기서 드러난다. VMI는 단순한 Pod가 아니라 controller가 여러 리소스를 엮어 만들어내는 가상화 워크로드이기 때문이다.

다음 글에서는 이 handoff를 받은 virt-handler가 노드에서 어떤 식으로 VM lifecycle을 이어받는지 살펴보겠다.

virt-controller Deep Dive: Creating VM Pods with Informers, Queues, and Reconcile

Introduction

The component closest to a "cluster-wide brain" in KubeVirt is virt-controller. While the actual VM execution happens on the node side with virt-handler and virt-launcher, the center that orchestrates when to create which Pod and when to advance which state to the next stage is virt-controller.

This article focuses on pkg/virt-controller/watch/application.go, pkg/virt-controller/watch/vmi/vmi.go, and pkg/virt-controller/watch/migration/migration.go to examine what informers this controller connects, what queues it runs, and how it creates launcher Pods.

virt-controller Is Not a Single Controller

The name alone might suggest a single process with a single reconcile loop, but in reality multiple watchers and controllers are bundled together. Looking at application.go, the following families are co-located:

  • VMI controller
  • VM controller
  • Migration controller
  • Replica set, pool, clone controllers
  • Snapshot, export, backup related controllers
  • Node, workload update, disruption budget related controllers

In other words, virt-controller is KubeVirt's cluster-wide orchestration collection.

Why So Many Informers

Looking at the NewController signature, even just the VMI controller receives multiple informers:

  • VMI informer
  • VM informer
  • Pod informer
  • PVC informer
  • Migration informer
  • Storage class informer
  • DataVolume, CDI informer
  • KubeVirt CR informer

The reason for so many is that a VMI's desired state is not determined by VMI spec alone.

  • Is storage ready?
  • Does a Pod already exist?
  • Is migration in progress?
  • Which feature gates has the cluster config enabled?
  • How should network annotations be generated?

The controller must synthesize all of this surrounding state to accurately create the launcher Pod.

The Typical Pattern Shown in watch/vmi/vmi.go

The VMI controller closely resembles the standard Kubernetes controller pattern.

1. Receive Events

Register event handlers on VMI, Pod, DataVolume, PVC, VM, and KubeVirt objects.

2. Enqueue Keys

Actual work is not done directly in event handlers but is passed to a queue.

3. Sync Loop Reads Current State

Reads VMI, Pod, PVC, and migration state from indexers and stores, comparing desired state with current state.

4. Create Necessary Pods or Status

Launcher Pod creation, annotation updates, network status updates, and expectation management happen here.

The advantage of this pattern is that it mitigates race conditions and enables idempotent reconciliation.

How Launcher Pods Are Created

The VMI controller does not manually assemble Pod YAML. It calls methods like RenderLaunchManifest through a templateService. In other words, the controller makes the "a Pod is needed" decision, and the template service renders the actual launcher Pod spec.

This separation is quite important:

  • The controller focuses on state judgment
  • The template service focuses on Pod spec composition

Thanks to this, variations like migration Pods and hotplug attachment Pods can be handled through separate rendering paths.

Why the Controller Knows About Network and Storage Directly

Many people ask here: "Don't network and storage attach at the node? Why does the controller have an annotation generator?"

The answer is pre-wiring work.

Looking at watch/vmi/vmi.go, the following dependencies exist:

  • Network annotations generator
  • Storage annotations generator
  • Network status updater
  • Network spec validator
  • Migration evaluator

The controller is not simply creating Pods -- it also prepares annotations and state so that when the Pod arrives at a node, KubeVirt and the CNI side have the context they need.

Why the Expectations Pattern Matters

When reading Kubernetes controllers, you frequently encounter the expectations pattern. KubeVirt is no different.

The VMI controller has:

  • Pod expectations
  • VMI expectations
  • PVC expectations

This pattern solves the problem of "I just sent a Pod creation request, so even though the informer cache hasn't reflected it yet, don't retry too early." It reduces duplicate creation when the controller reads a stale cache immediately after a create call.

In large-scale clusters, this pattern is very important. Because a single VMI has more complex surrounding resources than a regular Pod, duplicate creation causes much greater confusion.

When Does Responsibility Pass to virt-handler

docs/components.md explains that responsibility shifts to virt-handler after the Pod is scheduled to a node and nodeName is determined. The actual mental model is almost exactly this:

  1. virt-controller creates the launcher Pod.
  2. Kubernetes schedules the Pod to a specific node.
  3. The controller reflects Pod and VMI state.
  4. The node's virt-handler sees that VMI and takes over VM launch.

In other words, virt-controller is a handoff coordinator between the cluster scheduler and the node agent.

Why There Can Be Two Pods During Migration

This is what distinguishes KubeVirt's controller from a normal Pod controller. A VMI normally appears as "one VMI, one launcher Pod," but during migration a target Pod is additionally created.

So the migration controller side includes:

  • Target Pod creation
  • Source and target state tracking
  • Concurrent migration limits
  • Unschedulable timeout management

Looking at pkg/virt-controller/watch/migration/migration.go, you see mechanisms like pending timeout, priority queue, policy store, and handoff map. This is because migration is not a simple Pod reschedule but a complex operation moving a live guest.

The Full Orchestration Picture from application.go

Looking at the VirtControllerApp struct, you get a sense of what KubeVirt manages cluster-wide:

  • Informer factory
  • Various resource informers
  • Template service
  • Cluster config
  • Migration controller
  • Backup, export, snapshot controllers

The important fact you can learn here is that virt-controller has a much broader role than just being a VM Pod creator. It is the central orchestration layer for the entire VM lifecycle.

Common Misconceptions

Misconception 1: virt-controller Directly Runs VMs

No. virt-controller creates Pods and coordinates state. The actual VM process launch happens on the node side.

Misconception 2: The Controller Only Needs to Watch VMIs

No. It must look at Pods, PVCs, migrations, cluster config, and CDI state together to accurately create launcher Pods.

Misconception 3: Migration Is Just Rescheduling a Pod

No. Since it must maintain live guest state, network, and disk visibility while preparing a target Pod, a separate migration controller is needed.

Symptoms and Locations Operators Should Check

VMI Exists but Launcher Pod Isn't Created

  • virt-controller logs
  • DataVolume or PVC readiness state
  • Network spec validation errors

Migration Is Stuck in Pending for Too Long

  • Whether target Pod scheduling is possible
  • Migration controller timeout
  • Cluster-wide parallel migration limits

Pod Exists but State Is Misaligned

  • Informer cache reflection delay
  • Expectation-related retries
  • VMI status patch conflicts

Conclusion

virt-controller is KubeVirt's cluster-wide coordinator. This component reads the world around VMIs through various informers, creates launcher Pods through queue-based reconcile, and hands off responsibility to virt-handler at the appropriate time. The reason migration is complex also becomes apparent here -- a VMI is not a simple Pod but a virtualization workload that the controller creates by weaving together multiple resources.

In the next article, we will examine how virt-handler, having received this handoff, takes over the VM lifecycle on the node.