상태: 초안 (2026-10-16), 우리 코드 + 공식 문서 + 일부 실측. 동작 설명은 TT Lab 백엔드 코드(
backend/app/vms.py,backend/app/main.py)와 설계 문서를 그대로 따랐습니다. 버전은 글을 쓴 날 읽어서 확인한 값입니다(쿠버네티스 v1.34.10, KubeVirt v1.9.0, CDI v1.66.0). 시간 숫자는 실습을 만들면서 잰 기록이고, 표본 수와 날짜를 함께 적었습니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 문서에서 찾지 못한 것은 「미확인」으로 표시합니다.
학생 VM 이 뜨기까지 1: 네 층으로 본 전체 그림
목차
1. 왜 파드가 아니라 VM 인가
TT Lab 의 실습은 대부분 쿠버네티스 파드 하나에서 돕니다. 파드는 몇 초 만에 뜨고, 이미지만 바꾸면 환경이 바뀌고, 지우면 흔적이 남지 않습니다. 그런데 파드로는 끝내 흉내 낼 수 없는 실습이 있었습니다.
- systemd 가 없습니다. 서비스를 껐다 켜거나 유닛 파일을 고치는 실습은 「문법을 검사한다」 로 바꿀 수밖에 없었습니다.
- kubeadm 으로 노드를 세우고, etcd 를 백업하고 복구하는 일이 안 됩니다. 파드 안에서는 쿠버네티스를 kwok 으로 흉내 냈는데, kwok 은 컨테이너를 실제로 돌리지 않고 파드를 Ready 로 위조합니다.
- 커널 기능을 만질 수 없습니다. 실습 파드는 capability 가 0개라서 rootless podman 조차 돌지 않습니다. 그리고 그 0개는 TT Lab 이 지키는 원칙이라 늘릴 생각이 없습니다.
그래서 이런 실습만 가상머신으로 엽니다. 학생은 VM 안에서 진짜 root 이고 systemd·커널·디스크를 마음대로 다룹니다. 대신 그 root 가 VM 밖으로 나가지 못하게, 그리고 다 쓴 VM 이 반드시 회수되게 만드는 것이 이 시리즈의 주제입니다. VM 은 쿠버네티스 위에서 KubeVirt 로 띄웁니다. 실습 파드와 같은 클러스터, 같은 스케줄러, 같은 스토리지를 쓰기 때문입니다.
2. 네 층
이 시리즈는 일부러 네 층만 봅니다. 학생(브라우저), 백엔드(웹 파드), KubeVirt, 쿠버네티스 스토리지입니다. 게이트웨이·이미지 레지스트리·모니터링처럼 그 사이에 끼어 있는 것은 그림에서 뺐습니다. 「VM 이 할당된다」 는 일의 뼈대가 이 넷이기 때문입니다.
층마다 하는 일을 한 줄로 줄이면 이렇습니다.
| 층 | 하는 일 | 이 시리즈에서 |
|---|---|---|
| 학생 | 버튼을 누르고, 웹 터미널로 VM 안의 셸을 쓴다 | 1편 |
| 백엔드 | 자리를 확인하고, VM 에 심을 것을 준비하고, 루트 디스크를 고르고, VM 객체를 만든다. 다 쓰면 지운다 | 1·5·6편 |
| KubeVirt | VM 객체를 보고 파드를 만들고, 그 파드 안에서 QEMU 로 게스트를 띄운다 | 2편 |
| 스토리지 | 게스트의 디스크가 될 볼륨을 붙인다. 골든 PVC 하나를 여러 VM 이 나눠 읽는다 | 3·4편 |
3. 열 걸음
① 「실습 시작」
학생이 버튼을 누르면 브라우저가 POST /api/sessions 를 보냅니다. 레슨이 runtime: vm 이면 이 세션은 파드가 아니라 VM 을 만듭니다. 세션 기록에는 처음부터 runtime 이 들어갑니다. 회수기가 나중에 「무엇을 지울지」 를 이 값 하나로 정하기 때문입니다. 여기서 종류를 잘못 알면 파드는 지웠다고 기록되고 VM 은 메모리 4GiB 를 계속 붙잡습니다.
② 자리 확인
두 가지를 봅니다. 하나는 1인 1세션입니다. 다른 실습이 열려 있으면 무엇이 열려 있는지 알려 주고 새로 만들지 않습니다. 사용자별 잠금 안에서 확인과 기록을 함께 하므로 버튼을 두 번 눌러도 세션은 하나입니다.
다른 하나는 **동시에 떠 있는 VM 의 수(기본 8대)**입니다. 이 숫자는 DB 가 아니라 클러스터에 물어서 셉니다. DB 는 회수기가 놓친 유령 VM 을 세지 못하는데, 노드의 메모리를 실제로 잡고 있는 것은 클러스터 쪽이기 때문입니다. 상한에 걸리면 「지금 VM 실습이 8개 열려 있어 자리가 없습니다」 를 돌려줍니다. 왜 8인지는 5편에서 봅니다.
③ cloud-init Secret
VM 은 이미지를 미리 굽지 않고, 공용 클라우드 이미지에 첫 부팅 때 cloud-init 으로 필요한 것을 심습니다. 심는 것은 셋입니다.
- 채점 에이전트. 게스트 안에서 8899 포트로 듣는 작은 HTTP 서버입니다. 받은 명령을 셸로 돌려 채점합니다. 왜 이런 통로가 필요한지는 ⑧ 에서 봅니다.
- VM 마다 다른 토큰. 에이전트는 이 토큰이 맞는 요청만 받습니다. 학생은 자기 VM 의 root 라 토큰을 볼 수 있습니다. 막으려는 것은 학생이 아니라, 네트워크 정책이 잘못됐을 때 닿아 버릴 다른 파드입니다(6편).
- 실습 파일과 준비 스크립트. 채점기와 실습 재료입니다. 준비 스크립트가 실패하면 「준비됨」 표식을 찍지 않습니다.
이것을 VM 매니페스트에 바로 적지 않고 Secret 에 담는 데는 실측한 이유가 있습니다. KubeVirt 는 인라인 cloud-init 을 2048바이트로 막는데, 채점 에이전트를 실으면 7천 바이트가 넘습니다. 그래서 모든 실습 VM 이 같은 길(Secret)로 갑니다. 크기에 따라 길이 갈리면 에이전트를 넣는 순간에만 터지는 경로가 생기기 때문입니다.
④ 루트 디스크 고르기
여기가 이 시리즈의 중심입니다. 레슨이 vm_disk: "golden" 을 적었으면, 백엔드는 운영자가 미리 만들어 둔 골든 PVC 를 읽어(get 하나) 지금 써도 되는지 판정합니다. 쓸 수 있으면 그 PVC 를 읽기 전용 바탕으로 쓰고, VM 마다 얇은 쓰기 층만 얹습니다. 없거나 쓸 수 없으면(없음·권한 없음·가져오는 중·한 노드용 볼륨·다른 이미지·지우는 중) 오류를 내지 않고 기본 디스크(컨테이너 디스크)로 엽니다. 골든은 최적화이지 전제가 아니기 때문입니다. 디스크 세 갈래는 3편, 골든과 쓰기 층은 4편에서 자세히 봅니다.
⑤ VirtualMachine 만들기
백엔드가 쿠버네티스 API 에 VirtualMachine 객체 하나를 만듭니다. 이 객체에는 라벨이 붙습니다. 실습 세션이라는 표시, 세션 id, 그리고 어느 환경(개발·운영)의 백엔드가 만들었는지입니다. 마지막 라벨은 사고에서 나왔습니다. 개발과 운영 백엔드가 같은 VM 네임스페이스를 보는데, 한쪽 회수기가 자기 DB 에 없는 VM 을 고아로 보고 다른 쪽 학생의 VM 을 지웠습니다(5편).
cloud-init Secret 에는 이 VM 을 소유자로 적어, VM 이 지워지면 Secret 도 함께 가게 합니다. 다만 소유자 연결이 실패할 수 있어서, 지울 때 Secret 도 따로 한 번 더 지웁니다.
⑥ KubeVirt: VM → VMI → 파드 → QEMU
여기서부터는 KubeVirt 가 합니다. VM 을 보고 실행 중인 인스턴스(VMI)를 만들고, VMI 마다 virt-launcher 파드 하나를 만듭니다. 쿠버네티스 스케줄러가 그 파드를 노드에 앉히면, 그 노드의 virt-handler 가 파드 안의 libvirt 에게 「이 VM 을 켜라」 고 시킵니다. 게스트는 그 파드 안의 QEMU 프로세스로 돕니다. VM 이 파드 안에서 도는 까닭과 그 대가는 2편의 주제입니다.
⑦ 볼륨 붙이기
virt-launcher 파드가 만들어질 때 게스트의 디스크가 될 볼륨도 함께 붙습니다. 골든 실습이면 골든 PVC 가 읽기 전용으로 붙고, 그 위에 KubeVirt 가 파드의 임시 디렉터리(emptyDir)에 쓰기 층 파일을 만듭니다. 게스트에는 이 둘이 합쳐진 디스크 하나(/dev/vda)만 보입니다. 붙일 PVC 가 없으면 KubeVirt 는 파드를 아예 만들지 못하고 VM 은 영영 뜨지 않습니다. ④ 에서 미리 판정하는 이유입니다.
⑧ 준비 확인
백엔드는 두 단계로 기다립니다. 먼저 VMI 가 Running 이 되기를(실습마다 정한 상한, 기본 150초), 그다음 게스트 안의 에이전트가 「준비됨」 이라고 답하기를 기다립니다.
KubeVirt 에는 파드의 exec 같은 「게스트 안에서 명령 돌리기」 가 없습니다. 서브리소스는 console·vnc·portforward 같은 것뿐이고, 의도된 설계입니다. 그래서 준비 확인도 채점도 ③ 에서 심은 에이전트로 합니다.
Running 만 보고 학생을 들여보내면 안 된다는 것도 실제로 겪었습니다. 에이전트는 cloud-init 앞쪽에서 뜨고, 실습 환경을 세우는 준비 스크립트는 그 뒤에 돕니다. 그 사이에 들여보내니 1단계부터 「파일이 없습니다」 로 실패했고, 채점은 8단계 중 4단계만 통과했습니다. 지금은 cloud-init 의 마지막 줄이 준비 표식을 찍고, 준비 스크립트가 실패하면 찍지 않습니다. 그러면 백엔드는 시간 초과로 정직하게 실패합니다.
⑨ 콘솔 중계와 ⑩ 웹 터미널
백엔드가 VMI 의 console 서브리소스에 웹소켓으로 붙고(⑨), 학생 브라우저의 웹 터미널과 그 사이를 중계합니다(⑩). 여기에도 함정이 둘 있었습니다.
- 파드 터미널에 쓰던 쿠버네티스 클라이언트의 스트림 함수는 첫 바이트를 채널 번호로 읽습니다. KubeVirt 콘솔은 그런 껍데기가 없는 원시 바이트(
plain.kubevirt.io)라서, 같은 함수로 붙으면 연결은 열리는데 화면에 아무것도 나오지 않습니다. 오류도 나지 않아 더 헷갈렸습니다. 그래서 웹소켓으로 직접 붙습니다. - 콘솔 끝에 있는 것은 셸이 아니라 로그인 프롬프트(getty)입니다. 실습 VM 은 일부러 비밀번호를 만들지 않으므로, cloud-init 이 시리얼 콘솔에 root 자동 로그인을 걸어 둡니다.
웹소켓을 열 때 백엔드는 접속한 사람이 그 세션의 주인인지, 세션이 실행 중인지를 먼저 봅니다. 콘솔 서브리소스에 닿을 수 있는 쿠버네티스 신원은 백엔드 서비스 계정뿐입니다(6편).
4. 얼마나 걸리나 — 지금까지 잰 것
| 무엇 | 값 | 조건 | 표본 |
|---|---|---|---|
| VMI 단계: Pending → Scheduled → Running | 0초 → 10초 → 20초(10초 단위 기록) | 우분투 24.04 컨테이너 디스크, 2 vCPU·4Gi, 2026-08-29 | 1회 |
| 게스트 안 cloud-init 완료 | 23.8초 | 같은 VM | 1회 |
| 도커를 cloud-init 으로 설치하는 실습 | 약 76초(부팅 25 + apt 색인 14 + 설치 37) | 2026-08 실습 개발 중 | 1회 |
| 학생 API 로 연 세션, 요청부터 준비까지 | 90.991초 | Istio 실습 하나, 2026-09-12 | 1회 |
| VM 세 대짜리 세션: 세 VM Running | 17~23초 | kubeadm 3노드 실습, 2026-09-23~24 | 시험 여러 번의 범위 |
| 같은 세션: 세 노드 준비 스크립트 끝 | 약 300초 | 같은 실습 | 같음 |
| 실습마다 정해 둔 준비 상한의 근거 | k3s 82초 · Istio 94초 · k0s 150초 · Cilium 269초 | 각 실습의 정답지 검증 | 실습마다 |
| VM 마다 디스크를 새로 가져오는 방식(DataVolume)의 첫 부팅 | 최대 195초 | 3편 | 최대값만 기록 |
표가 말해 주는 것은 두 가지입니다. VM 자체는 20초 안팎에 켜지고, 나머지는 게스트 안에서 실습을 세우는 시간입니다. 그리고 디스크를 VM 마다 새로 가져오면 그것만으로 몇 분이 듭니다. 4편의 골든 디스크가 노리는 것이 이 마지막 줄입니다.
한계도 분명합니다. 대부분 한 번씩 잰 값이고, 학생 세션 전체의 준비 시간 분포(중앙값과 흩어짐)는 미실측입니다. 세션 기록에 시작 시각과 준비 시각이 함께 남으므로 기록만 모으면 잴 수 있습니다.
5. 이 그림이 일부러 뺀 것
- 네트워크 정책. 학생 VM 으로 들어오는 연결은 기본 차단이고 백엔드만 8899 로 닿습니다. 나가는 쪽은 인터넷만 열려 있습니다. 6편에서 봅니다.
- 회수. 세션 시간이 다 되면 회수기가 VM 을 지우고, 쓰기 층은 파드와 함께 사라집니다. 5편에서 봅니다.
- VM 여러 대짜리 세션. kubeadm 실습처럼 노드 셋이 필요한 세션은 같은 걸음(②~⑧)을 VM 마다 되풀이하고, 세션 전용 망을 하나 더 깝니다. 이 시리즈는 한 대짜리를 기준으로 씁니다.
티티 한마디. 버튼 한 번 뒤에 열 걸음이 숨어 있어요. 그런데 VM 이 켜지는 건 20초면 끝나고, 나머지는 거의 「게스트 안에서 실습 준비하기」 와 「디스크 마련하기」 예요. 그러니 빨라지고 싶으면 디스크부터 보세요. 4편에서 만나요!
참고
문서는 모두 2026-10-16 에 열람했습니다.
- KubeVirt 구성요소: https://github.com/kubevirt/kubevirt/blob/main/docs/components.md
- KubeVirt 디스크와 볼륨: https://kubevirt.io/user-guide/storage/disks_and_volumes/
- TT Lab 코드:
backend/app/vms.py(매니페스트·cloud-init·골든 판정),backend/app/main.py(create_session·make_vm·vm_console·reap_once),backend/app/vm_agent.py(채점 에이전트) - 설계와 실측 기록:
docs/plans/2026-08-29-vm-labs-design.md,docs/plans/2026-09-12-vm-reaper-environment.md,docs/plans/2026-09-24-multi-vm-sessions.md,docs/plans/2026-10-14-vm-golden-overlay.md