Skip to content

Split View: 사용자 요청이 VMI로 바뀌는 순간: virt-api와 admission 흐름

|

사용자 요청이 VMI로 바뀌는 순간: virt-api와 admission 흐름

들어가며

KubeVirt를 "VM을 위한 Kubernetes 확장"이라고 부를 때, 그 확장의 가장 눈에 띄는 표면은 API다. 사용자는 kubectl이나 client를 통해 VM, VMI, migration 요청을 보낸다. 하지만 이 요청은 그대로 node로 내려가지 않는다. 중간에는 검증, 기본값 적용, subresource 분기, 권한 확인, 작업 객체 생성이 있다.

이 레이어를 담당하는 대표 컴포넌트가 virt-api다. docs/components.mdvirt-api-server를 virtualization flow의 entry point로 설명한다.

virt-api는 무엇을 하는가

간단히 말해 virt-api는 다음 역할을 맡는다.

  • KubeVirt 관련 API의 진입점 제공
  • VMI, VM 관련 요청의 validation과 defaulting
  • start, stop, migrate 같은 subresource 요청 처리
  • 필요한 경우 내부적으로 다른 CR 생성

여기서 중요한 점은 virt-api가 직접 VM을 띄우지 않는다는 점이다. virt-api사용자 의도를 Kubernetes 오브젝트 연산으로 번역하는 데 집중한다.

왜 별도의 API 레이어가 필요한가

Kubernetes CRD만 있으면 충분해 보일 수 있다. 하지만 실제 운영에는 다음 요구가 있다.

  • 단순 create 또는 update만으로 표현하기 어려운 액션성 요청
  • guest 상태를 확인하고 충돌을 막는 사전 검증
  • migration처럼 별도 작업 객체를 생성해야 하는 요청
  • console, VNC, restart 같은 subresource 흐름

이런 이유 때문에 KubeVirt는 CRD 정의만으로 끝내지 않고, 별도의 API 처리 레이어를 둔다.

소스에서 보이는 virt-api의 성격

pkg/virt-api/rest/lifecycle.go를 보면 start, stop, migrate, reboot, backup 같은 다양한 요청 handler가 구현되어 있다. 여기서 패턴은 상당히 일관적이다.

  1. 대상 VM 또는 VMI를 조회한다.
  2. 현재 상태가 액션을 허용하는지 검증한다.
  3. 직접 실행하지 않고, 상태 패치나 작업 객체 생성을 수행한다.
  4. 이후 실제 실행은 controller 또는 virt-handler 계층으로 넘긴다.

virt-api는 orchestration의 시작점이지 최종 실행점이 아니다.

migrate 요청이 실제로 어떻게 바뀌는가

가장 좋은 예시는 migrate handler다. MigrateVMRequestHandler를 보면 내부 흐름은 매우 명료하다.

1. VM과 VMI를 조회한다

먼저 VM이 존재하는지, 해당 VM의 VMI가 존재하는지 확인한다.

2. 현재 VMI가 Running 상태인지 검증한다

실행 중이 아닌 VMI는 migration할 수 없으므로 conflict를 낸다.

3. migration CR을 생성한다

핵심은 여기다. handler는 직접 migration을 시작하지 않는다. 대신 VirtualMachineInstanceMigration 객체를 생성한다.

apiVersion: kubevirt.io/v1
kind: VirtualMachineInstanceMigration
metadata:
  generateName: kubevirt-migrate-vm-
spec:
  vmiName: demo-vm

실제 코드에서는 여기에 AddedNodeSelector 같은 옵션도 붙을 수 있다. 이 순간부터 migration은 API 요청이 아니라 controller가 소비할 declarative work item이 된다.

왜 이 방식이 중요한가

이 구조의 장점은 크다.

1. 작업이 Kubernetes 오브젝트로 남는다

migration이 별도 CR이 되므로 감사 추적과 상태 확인이 쉽다.

2. controller가 비동기적으로 처리할 수 있다

API 서버는 작업을 즉시 끝내지 않고 accepted를 반환할 수 있다.

3. 정책과 검증이 더 분리된다

API는 요청 유효성을 보고, 실제 용량과 정책 판단은 controller가 계속 수행한다.

start 또는 stop은 왜 migration과 다르게 보이는가

모든 액션이 migration CR 생성으로 번역되는 것은 아니다. 예를 들어 stop 요청은 경우에 따라 VM status patch나 state change request로 표현된다. lifecycle.gopatchVMStatusStoppedgetChangeRequestJson은 이 패턴을 잘 보여준다.

즉 KubeVirt는 액션마다 가장 자연스러운 Kubernetes 표현을 선택한다.

  • long-running 이동 작업: 별도 migration CR
  • VM lifecycle 전환: status patch 또는 state change request
  • node-side 즉시 연결이 필요한 작업: virt-handler URI 기반 서브리소스

subresource가 필요한 이유

일반적인 spec update만으로는 "지금 migration을 시작해", "지금 soft reboot 해", "console에 연결해" 같은 요청을 자연스럽게 표현하기 어렵다. 그래서 KubeVirt는 subresource를 둔다.

이때 중요한 설계 포인트는 다음과 같다.

  • 요청은 API 레이어에서 인증과 검증을 통과해야 한다
  • 런타임 상태를 확인해 conflict를 빠르게 반환해야 한다
  • 실제 하위 동작은 적합한 컴포넌트로 위임해야 한다

이것은 Kubernetes 본체가 scale, exec, log, eviction 같은 subresource를 두는 이유와 비슷하다.

virt-apivirt-handler의 차이

헷갈리기 쉬운 지점이라 분리해서 보자.

virt-api

  • 사용자와 Kubernetes API 가까이에 있음
  • validation, defaulting, subresource 진입점
  • 상태 패치 또는 작업 객체 생성

virt-handler

  • 노드 가까이에 있음
  • 실제 launcher Pod와 통신
  • 도메인 상태 반영
  • VM 실행, 종료, migration handoff 관여

둘 다 "VM을 제어한다"라고 느껴질 수 있지만, virt-api는 control plane entry이고 virt-handler는 node execution plane 쪽에 가깝다.

실전에서 봐야 할 API 레이어 증상

migrate 요청이 바로 실패한다

API 레이어 conflict일 가능성이 크다. 대표적으로:

  • VMI가 Running이 아님
  • paused 상태
  • 액션 불가 조건

요청은 accepted 되었는데 아무 일도 안 일어난다

이 경우는 API는 통과했지만 controller 단계에서 용량, 정책, pending pod 문제로 멈췄을 수 있다.

어떤 요청은 patch이고 어떤 요청은 CR 생성인 이유가 헷갈린다

핵심은 "이 요청이 상태 전환인가, 독립 작업인가"를 기준으로 보면 된다.

운영자가 기억해야 할 디버깅 순서

  1. 사용자가 보낸 요청이 어떤 subresource로 들어가는지 본다.
  2. virt-api가 conflict를 냈는지 본다.
  3. accepted 이후라면 어떤 오브젝트가 추가로 생성되었는지 본다.
  4. 그 다음에야 controller나 node agent로 내려간다.

이 순서를 거꾸로 보면 문제를 놓치기 쉽다.

마무리

virt-api의 핵심 역할은 VM을 직접 실행하는 것이 아니라, 사용자 요청을 검증 가능한 Kubernetes 연산으로 바꾸는 것이다. migrate 요청은 migration CR 생성으로, stop 요청은 상태 변경 패치로, 일부 런타임 액션은 virt-handler 연결로 번역된다. 이 레이어를 이해하면 KubeVirt가 왜 Kubernetes스럽게 동작하는지, 또 왜 요청과 실행이 시간적으로 분리되는지 선명하게 보인다.

다음 글에서는 이 요청과 객체를 실제 launcher Pod 생성으로 연결하는 virt-controller의 informer, queue, reconcile 구조를 살펴보겠다.

The Moment a User Request Becomes a VMI: virt-api and Admission Flow

Introduction

When we call KubeVirt "a Kubernetes extension for VMs," the most visible surface of that extension is the API. Users send VM, VMI, and migration requests through kubectl or clients. But these requests don't go directly down to the node. In between, there is validation, defaulting, subresource branching, permission checking, and work object creation.

The primary component responsible for this layer is virt-api. docs/components.md also describes virt-api-server as the entry point for the virtualization flow.

What Does virt-api Do

Simply put, virt-api is responsible for:

  • Providing entry points for KubeVirt-related APIs
  • Validation and defaulting of VMI and VM related requests
  • Handling subresource requests like start, stop, migrate
  • Creating other CRs internally when needed

The important point here is that virt-api does not directly launch VMs. virt-api focuses on translating user intent into Kubernetes object operations.

Why Is a Separate API Layer Needed

It might seem sufficient to just have Kubernetes CRDs. But actual operations have the following requirements:

  • Action-oriented requests that are hard to express with just create or update
  • Pre-validation that checks guest state and prevents conflicts
  • Requests that need to create separate work objects, like migration
  • Subresource flows like console, VNC, restart

For these reasons, KubeVirt doesn't stop at CRD definitions alone but places a separate API processing layer.

The Character of virt-api as Seen in Source

Looking at pkg/virt-api/rest/lifecycle.go, you can see various request handlers for start, stop, migrate, reboot, backup, etc. The patterns here are quite consistent:

  1. Look up the target VM or VMI.
  2. Validate whether the current state allows the action.
  3. Instead of executing directly, perform a status patch or work object creation.
  4. Delegate actual execution to the controller or virt-handler layer afterward.

In other words, virt-api is the starting point of orchestration, not the final execution point.

How a Migrate Request Actually Transforms

The best example is the migrate handler. Looking at MigrateVMRequestHandler, the internal flow is very clear.

1. Look Up VM and VMI

First, it checks whether the VM exists and whether the VMI for that VM exists.

2. Validate That the Current VMI Is in Running State

A VMI that is not running cannot be migrated, so it returns a conflict.

3. Create a Migration CR

This is the key. The handler does not start migration directly. Instead, it creates a VirtualMachineInstanceMigration object.

apiVersion: kubevirt.io/v1
kind: VirtualMachineInstanceMigration
metadata:
  generateName: kubevirt-migrate-vm-
spec:
  vmiName: demo-vm

In the actual code, options like AddedNodeSelector may also be attached. From this moment, the migration is no longer an API request but a declarative work item for the controller to consume.

Why This Approach Matters

The advantages of this structure are significant.

1. Work Persists as Kubernetes Objects

Since migration becomes a separate CR, audit tracking and status checking are easy.

2. Controllers Can Process Asynchronously

The API server can return accepted without completing the work immediately.

3. Policy and Validation Are Further Separated

The API checks request validity, while actual capacity and policy decisions continue to be handled by the controller.

Why Start or Stop Looks Different from Migration

Not all actions are translated into migration CR creation. For example, a stop request may be expressed as a VM status patch or state change request depending on the case. patchVMStatusStopped and getChangeRequestJson in lifecycle.go demonstrate this pattern well.

In other words, KubeVirt chooses the most natural Kubernetes expression for each action:

  • Long-running move operations: separate migration CR
  • VM lifecycle transitions: status patch or state change request
  • Operations requiring immediate node-side connection: virt-handler URI-based subresources

Why Subresources Are Needed

Simple spec updates alone cannot naturally express requests like "start migration now," "perform a soft reboot now," or "connect to console." So KubeVirt uses subresources.

The important design points here are:

  • Requests must pass authentication and validation at the API layer
  • Runtime state must be checked to quickly return conflicts
  • Actual downstream operations must be delegated to the appropriate component

This is similar to why Kubernetes itself has subresources like scale, exec, log, and eviction.

Difference Between virt-api and virt-handler

This is an easy-to-confuse point, so let's separate them:

virt-api

  • Close to the user and Kubernetes API
  • Validation, defaulting, subresource entry point
  • Status patching or work object creation

virt-handler

  • Close to the node
  • Communicates with actual launcher Pods
  • Reflects domain state
  • Involved in VM execution, termination, migration handoff

Both may feel like they "control VMs," but virt-api is the control plane entry while virt-handler is closer to the node execution plane.

API Layer Symptoms to Watch in Practice

Migrate Request Fails Immediately

High probability of an API layer conflict. Typically:

  • VMI is not Running
  • Paused state
  • Action not permitted condition

Request Is Accepted but Nothing Happens

In this case, the API passed but may be stuck at the controller stage due to capacity, policy, or pending pod issues.

Confusion About Why Some Requests Are Patches and Others Are CR Creations

The key is to look at whether "this request is a state transition or an independent operation."

Debugging Order Operators Should Remember

  1. See which subresource the user's request entered through.
  2. Check if virt-api returned a conflict.
  3. If accepted, see what additional objects were created.
  4. Only then go down to the controller or node agent.

If you look at this order in reverse, you easily miss problems.

Conclusion

The core role of virt-api is not to directly run VMs but to convert user requests into verifiable Kubernetes operations. Migrate requests are translated into migration CR creation, stop requests into status change patches, and some runtime actions into virt-handler connections. Understanding this layer makes it clear why KubeVirt behaves in a Kubernetes-like manner, and why requests and execution are temporally separated.

In the next article, we will examine the informer, queue, and reconcile structure of virt-controller that connects these requests and objects to actual launcher Pod creation.