- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며
- virt-api는 무엇을 하는가
- 왜 별도의 API 레이어가 필요한가
- 소스에서 보이는 virt-api의 성격
- migrate 요청이 실제로 어떻게 바뀌는가
- 왜 이 방식이 중요한가
- start 또는 stop은 왜 migration과 다르게 보이는가
- subresource가 필요한 이유
- virt-api와 virt-handler의 차이
- 실전에서 봐야 할 API 레이어 증상
- 운영자가 기억해야 할 디버깅 순서
- 마무리
들어가며
KubeVirt를 "VM을 위한 Kubernetes 확장"이라고 부를 때, 그 확장의 가장 눈에 띄는 표면은 API다. 사용자는 kubectl이나 client를 통해 VM, VMI, migration 요청을 보낸다. 하지만 이 요청은 그대로 node로 내려가지 않는다. 중간에는 검증, 기본값 적용, subresource 분기, 권한 확인, 작업 객체 생성이 있다.
이 레이어를 담당하는 대표 컴포넌트가 virt-api다. docs/components.md도 virt-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가 구현되어 있다. 여기서 패턴은 상당히 일관적이다.
- 대상 VM 또는 VMI를 조회한다.
- 현재 상태가 액션을 허용하는지 검증한다.
- 직접 실행하지 않고, 상태 패치나 작업 객체 생성을 수행한다.
- 이후 실제 실행은 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.go의 patchVMStatusStopped와 getChangeRequestJson은 이 패턴을 잘 보여준다.
즉 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-api와 virt-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 생성인 이유가 헷갈린다
핵심은 "이 요청이 상태 전환인가, 독립 작업인가"를 기준으로 보면 된다.
운영자가 기억해야 할 디버깅 순서
- 사용자가 보낸 요청이 어떤 subresource로 들어가는지 본다.
virt-api가 conflict를 냈는지 본다.- accepted 이후라면 어떤 오브젝트가 추가로 생성되었는지 본다.
- 그 다음에야 controller나 node agent로 내려간다.
이 순서를 거꾸로 보면 문제를 놓치기 쉽다.
마무리
virt-api의 핵심 역할은 VM을 직접 실행하는 것이 아니라, 사용자 요청을 검증 가능한 Kubernetes 연산으로 바꾸는 것이다. migrate 요청은 migration CR 생성으로, stop 요청은 상태 변경 패치로, 일부 런타임 액션은 virt-handler 연결로 번역된다. 이 레이어를 이해하면 KubeVirt가 왜 Kubernetes스럽게 동작하는지, 또 왜 요청과 실행이 시간적으로 분리되는지 선명하게 보인다.
다음 글에서는 이 요청과 객체를 실제 launcher Pod 생성으로 연결하는 virt-controller의 informer, queue, reconcile 구조를 살펴보겠다.