TT Lab
요금제

한국어

시작하기
블로그

블로그

KubeVirt 소스 코드 읽기 2: API 와 CRD — 타입, 생성 코드, 웹훅

상태: 초안 (2026-10-17), 소스 + 읽기 전용 실측. 기준은 KubeVirt v1.9.0(커밋 79d34c17)입니다. 웹훅 경고 문구는 소스에서 읽었고, 실제 kubectl 화면에서 본 것은 아닙니다(VM 을 새로 만들지 않았습니다).

KubeVirt 소스 코드 읽기 2: API 와 CRD

목차

1. 직관: 세 군데에서 정해진다

VM 의 YAML 한 줄이 받아들여지기까지 세 군데가 관여합니다.

  1. 타입 — types.go 의 Go 구조체가 필드 이름과 모양을 정합니다.
  2. 스키마 — 그 구조체에서 생성된 OpenAPI 스키마가 CRD 에 실려, kube-apiserver 가 모양이 틀린 요청을 먼저 걸러 냅니다.
  3. 웹훅 — 모양은 맞아도 뜻이 틀린 요청(둘이 동시에 쓰이면 안 되는 필드, 꺼진 기능 게이트)은 virt-api 의 웹훅이 거릅니다. 거절하지 않고 경고만 붙일 수도 있습니다.

2. 타입: types.go

staging/src/kubevirt.io/api/core/v1/ 아래의 types.go 한 파일이 4,090줄입니다. 먼저 두 구조체만 찾으면 됩니다.

Running 필드에는 Deprecated: 주석이 붙어 있습니다(:2010). 그래도 지우지 않고 남겨 둔 이유는 메서드 하나에 보입니다. 두 필드를 하나의 실행 전략으로 접는 RunStrategy() (:1957)입니다.

RunStrategy := RunStrategyHalted
if vm.Spec.Running != nil {
	if (*vm.Spec.Running) == true {
		RunStrategy = RunStrategyAlways
	}
} else if vm.Spec.RunStrategy != nil {
	RunStrategy = *vm.Spec.RunStrategy
}

출처: kubevirt/kubevirt v1.9.0 types.go 1961–1968행(Apache-2.0). 컨트롤러는 이 메서드만 부르므로, running: true 는 코드 안에서 곧바로 Always 가 됩니다. 실행 전략 상수는 다섯이 문서에 나오지만 코드에는 여섯 번째 WaitAsReceiver (:1995)도 있습니다. 클러스터 사이 마이그레이션을 받는 쪽 VM 을 위한 값이고, 기능 게이트가 켜졌을 때만 웹훅이 받아 줍니다(아래 4절).

VMI 의 상(phase) 목록(:1098)은 3·4편을 읽을 때 지도가 됩니다. Scheduled 에 달린 주석이 「여기서 virt-controller 의 책임이 끝나고 virt-handler 가 넘겨받는다」 고 말합니다(:1109).

3. 생성 코드: 손대지 않는 파일들

types.go 한 파일에서 생겨나는 것들 make generate → hack/generate.sh. 생성 파일은 손으로 고치지 않는다 staging/src/kubevirt.io/api/core/v1/types.go Go 구조체 + 주석 마커(+genclient, +kubebuilder…) · 4,090줄 deepcopy-gen deepcopy_generated.go 7,410줄 swagger-doc types_swagger_generated.go 1,312줄 openapi-gen openapi_generated.go 40,507줄 · client-go/api client-gen client-go 클라이언트 kubevirt.io/client-go controller-gen crd → crd-validation-generator controller-gen 결과는 virt-operator 의 validations_generated.go(33,701줄)로 들어가 설치 때 CRD 의 openAPIV3Schema 가 된다(4절 그림의 ③ 관문). CI 는 make generate 뒤 차이가 있으면 떨어뜨린다(generate-verify).
그림: types.go 하나에서 생겨나는 생성 파일들

make generate(Makefile:53)는 빌더 컨테이너 안에서 hack/generate.sh 를 돌립니다. 이 스크립트가 순서대로 부르는 생성기는 이렇습니다.

생성기 입력 출력 실측 줄 수
swagger-doc types.go 의 주석 types_swagger_generated.go(API 문서의 설명) 1,312
deepcopy-gen(generate.sh:27) +k8s:deepcopy-gen 마커 deepcopy_generated.go 7,410
defaulter-gen 기본값 함수 zz_generated.defaults.go —
openapi-gen 타입 전체 client-go/api/openapi_generated.go 40,507
client-gen +genclient 마커 client-go 의 타입 있는 클라이언트 —
controller-gen crd(:139) +kubebuilder: 마커 CRD YAML → crd-validation-generator(:177) → virt-operator/…/validations_generated.go 33,701

마지막 줄이 특이합니다. KubeVirt 는 CRD YAML 을 저장소에 두지 않고, controller-gen 이 만든 스키마를 Go 문자열로 바꿔 virt-operator 안에 넣습니다. virt-operator 가 설치할 때 NewVirtualMachineCrd() (crds.go:187)로 CRD 를 코드로 만들고, CRDsValidation["virtualmachine"] (validations_generated.go:4444) 을 openAPIV3Schema 로 붙입니다. 그래서 필드 하나를 더하는 PR 은 types.go 한 줄에 생성 파일 수백 줄이 따라옵니다. 리뷰어는 생성 파일을 읽지 않고, CI 가 make generate 를 다시 돌려 차이가 없는지만 봅니다(generate-verify). 손으로 고친 생성 파일은 여기서 떨어집니다.

4. 웹훅: 요청이 지나는 다섯 관문

VM 하나를 apply 하면 — API 서버 안의 관문 왼쪽이 kube-apiserver 의 순서, 오른쪽이 그 관문을 맡은 KubeVirt 코드 ① 인증 · 인가 RBAC ② 변형 웹훅 기본값 채우기 virt-api /virtualmachines-mutate VMsMutator.Mutate ③ 스키마 검증 CRD 의 openAPIV3Schema virt-operator CRD 를 설치 validations_generated.go ④ 검증 웹훅 허용 · 거절 · 경고 virt-api /virtualmachines-validate VMsAdmitter.Admit ⑤ etcd 에 저장 그 뒤 컨트롤러가 본다 ④ 가 돌려준 경고는 kubectl 에 「Warning:」 줄로 보인다 spec.running is deprecated, please use spec.runStrategy instead. 두 웹훅 모두 failurePolicy: Fail — virt-api 가 응답하지 않으면 VM 을 만들지도 고치지도 못한다(실측: 검증 23개 · 변형 4개 모두 Fail)
그림: apply 한 VM 이 kube-apiserver 안에서 지나는 관문과 그 코드

누가 웹훅을 등록하나

웹훅 설정 객체(ValidatingWebhookConfiguration)도 virt-operator 가 코드로 만듭니다. NewVirtAPIValidatingWebhookConfiguration()(webhooks.go:296) 이 경로마다 웹훅을 하나씩 넣고 failurePolicy 를 Fail 로 둡니다. VM 의 경로는 상수 VMValidatePath = "/virtualmachines-validate" (:945)입니다. 반대편에서 virt-api 가 같은 상수로 HTTP 처리기를 겁니다(api.go:994). 경로 이름을 한쪽에서만 바꾸면 웹훅이 404 를 받고, Fail 정책 때문에 VM 을 아무도 만들 수 없게 됩니다. 두 파일이 같은 상수를 쓰는 까닭입니다.

VMsAdmitter.Admit() 을 한 줄씩

vms-admitter.go:87 의 Admit() 은 이 순서로 검사합니다.

  1. 요청 자원이 정말 virtualmachines 인지, 스키마에 맞는지(ValidateSchema).
  2. 지우는 중인 VM 이면 바로 허용합니다. 다른 삭제와 경쟁하지 않으려는 것입니다.
  3. 복사본에 instancetype·preference 를 적용하고 기본값을 채운 뒤 그 결과를 검증합니다. 주석에 「저장하고 싶지 않아서 복사본에 적용한다」 고 적혀 있습니다. 즉 검증은 「실제로 뜰 VMI」 를 기준으로 하지만, etcd 에는 사용자가 보낸 그대로 들어갑니다.
  4. 만들 때(Create)만: 기능 게이트가 꺼진 필드를 쓰지 않았는지, 네트워크 설정이 맞는지.
  5. ValidateVirtualMachineSpec() — 템플릿, 메타데이터, VMI 스펙 전체, DataVolume 틀, 실행 전략, 실시간 갱신 기능.
  6. 스토리지 쪽 검사(storageadmitters.Admit)와 볼륨 요청.
  7. 통과하면 경고를 모아 허용합니다.

마지막 단계가 spec.running 경고가 나오는 곳입니다(:175–178).

warnings := warnDeprecatedAPIs(&vm.Spec.Template.Spec, admitter.ClusterConfig)
if vm.Spec.Running != nil {
	warnings = append(warnings, "spec.running is deprecated, please use spec.runStrategy instead.")
}

출처: kubevirt/kubevirt v1.9.0 vms-admitter.go 175–178행(Apache-2.0). 거절이 아니라 AdmissionResponse.Warnings 라서 VM 은 그대로 만들어지고, kubectl 은 응답 헤더의 경고를 Warning: 줄로 보여 줍니다. warnDeprecatedAPIs() (vmi-create-admitter.go:141)는 사용 중단 상태가 된 기능 게이트를 스펙이 쓰고 있을 때 경고를 더합니다.

실행 전략 검사

validateRunStrategy()(:231)는 세 가지를 봅니다. running 과 runStrategy 를 함께 쓰면 거절, 둘 다 없으면 「RunStrategy must be specified」 로 거절, 그리고 runStrategy 값이 허용 목록 validRunStrategies(:53) 다섯 중 하나인지입니다. WaitAsReceiver 는 이 목록에 없고, DecentralizedLiveMigration 기능 게이트가 켜졌을 때만 따로 통과합니다.

웹훅 말고 또 하나의 입구: 서브리소스

virt-api 는 웹훅 서버이면서 집계 API 서버(v1.subresources.kubevirt.io)이기도 합니다. virtctl start 는 VM 을 직접 고치지 않고 …/virtualmachines/<이름>/start 를 부릅니다. 처리기 StartVMRequestHandler() (lifecycle.go:43)는 실행 전략을 보고 Halted 면 VM 의 spec.runStrategy(또는 옛 spec.running)를 JSON 패치로 바꾸고, Manual·RerunOnFailure 면 status 에 시작 요청을 넣습니다. Always 처럼 이미 켜져 있어야 하는 전략에는 의미가 없다고 주석에 적혀 있습니다. 결국 서브리소스도 VM 객체를 고칠 뿐이고, 실제로 VMI 를 만드는 일은 3편의 VM 컨트롤러가 합니다.

5. 실측

v1.9.0 이 깔린 작은 클러스터에서 읽었습니다(2026-10-17).

6. 한계

티티 한마디. 필드 하나 더하는 PR 이 수백 줄인 걸 보고 놀라지 마세요. 진짜로 사람이 쓴 건 types.go 몇 줄이고 나머지는 기계가 쓴 거예요. 리뷰할 때는 types.go 와 웹훅, 그리고 시험만 보면 돼요!

참고

모두 2026-10-17 에 열람했습니다.

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다