16 KiB
실험대 가상화 계층 — 실측 기록
이 문서가 무엇인가
docs/guides/ 가 「무엇을 어떤 순서로 치는가」 를 적는다면, 이
문서는 「그때 실제로 어떤 값이 나왔는가」 를 적는다. 가이드에 이미 있는
절차는 반복하지 않는다.
여기 적힌 숫자는 전부 2026-09-10 에 test-server 에서 실제로 돌려 받은
출력이다. 추정값·예상값은 없다. 없는 값은 「미측정」이라고 쓴다.
| 이 문서가 답하는 것 | 가이드가 답하는 것 |
|---|---|
| 5120MB 를 줬는데 실제로 얼마를 쓰나 | 메모리를 얼마로 주나 |
| 20GB 오버레이가 디스크를 얼마나 먹나 | 오버레이를 어떻게 만드나 |
| cloud-init 이 몇 초 걸리나 | cloud-init 을 어떻게 쓰나 |
| 철거하면 무엇이 남나 | 무엇을 세우나 |
1. 측정 환경
lscpu | grep -E "^Model name|^CPU\(s\):|^Thread|^Core"
free -m | head -2
df -h /
virsh --version; qemu-system-x86_64 --version | head -1; uname -r
실측
CPU(s): 8
Model name: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
Thread(s) per core: 2
Core(s) per socket: 4
total used free shared buff/cache available
Mem: 11648 5642 2599 4 3776 6005
/dev/nvme0n1p3 226G 9.9G 204G 5% /
12.7.0
QEMU emulator version 11.1.1
7.2.2-arch1-1
어디를 봐야 하는가 — Core(s) per socket 4 에 Thread(s) per core 2 라
논리 코어가 8 이다. VM 에 주는 vCPU 는 물리 코어가 아니라 논리 코어를 나눠
쓰는 것이고, 합이 8 을 넘어도 libvirt 는 막지 않는다(오버커밋). 이 실험대는
2 + 2 + 1 = 5 로 잡아 여유를 뒀다.
free 의 available(6005MB)이 free(2599MB)보다 훨씬 큰 것이 정상이다 —
buff/cache 3776MB 는 필요하면 회수된다. VM 을 얼마나 더 띄울 수 있는지는
free 가 아니라 available 로 본다.
중첩 가상화
lscpu | grep Virtualization
cat /sys/module/kvm_intel/parameters/nested
Virtualization: VT-x
Y
켜져 있지만 이 실험대는 쓰지 않는다. 일회용으로 만들려는 층(게스트)은 이미 일회용이고, 비싼 층(호스트의 인증서·DNS)은 중첩으로 해결되지 않는다. 그리고 A-6 의 지연 주입처럼 시간을 재는 실험에서 중첩은 측정값을 왜곡한다.
2. 자원 — 할당과 실사용은 다르다
VM 에 준 메모리는 상한이지 점유가 아니다. virtio-balloon 이 안 쓰는 만큼 호스트에 돌려준다.
for v in kc-lab-1 kc-lab-2; do
printf "%-10s 할당 %sMB 실사용 %sMB\n" "$v" \
"$(( $(virsh dommemstat $v | awk '/actual/{print $2}') / 1024 ))" \
"$(( ($(virsh dommemstat $v | awk '/actual/{print $2}') \
- $(virsh dommemstat $v | awk '/^unused/{print $2}')) / 1024 ))"
done
실측 — k3s 만 떠 있고 Keycloak 은 아직 안 올린 상태
kc-lab-1 할당 5120MB 실사용 353MB
kc-lab-2 할당 3120MB 실사용 301MB
어디를 봐야 하는가 — 두 가지다.
① 실사용이 할당의 7% 밖에 안 된다. k3s server 가 353MB 다. 「5GB 를 줬으니 5GB 를 쓴다」가 아니다.
② kc-lab-2 의 할당이 4096 이 아니라 3120 이다. virt-install --memory 4096
으로 만들었는데 줄어 있다. virtio-balloon 이 회수해 간 것으로 보인다.
dommemstat 의 actual 은 현재 할당이지 선언한 상한이 아니다. 상한은
virsh dominfo 의 Max memory 에 있다 — 다만 이 실험대에서 그 값을 나란히
찍어 보지는 않았다(미측정). 둘을 헷갈리면 「메모리가 왜 줄었지」가 된다.
이 결과가 의미하는 것 — 합계 8240MB 를 할당했지만 실제 점유는 654MB 다. 그래서 11.6GB 짜리 호스트에서 VM 세 대가 무리 없이 돈다. 다만 이것은 지금 k3s 만 떠 있어서다 — Keycloak 2 파드 + PostgreSQL + Redis + Prometheus 가 올라가면 늘어난다. 그 시점의 값은 미측정이다.
3. 디스크 — 오버레이는 얼마나 쓰나
게스트 디스크는 base.qcow2 위의 오버레이다. 20GB 를 두 개 만들어도
바닥 이미지는 한 벌이고 변경분만 쌓인다.
qemu-img info /var/lib/libvirt/images/base.qcow2 | head -4
ls -l /var/lib/libvirt/images/
실측 — 엣지를 만들기 전, k3s 2 노드만 있던 시점이다.
image: /var/lib/libvirt/images/base.qcow2
file format: qcow2
virtual size: 3 GiB (3221225472 bytes)
disk size: 335 MiB
-rw-r--r-- base.qcow2 351404032 (335 MiB)
-rw------- kc-lab-1.qcow2 1521025024 (1.4 GiB) ← 선언 20GB
-rw------- kc-lab-2.qcow2 697499648 (665 MiB) ← 선언 20GB
-rw------- seed-kc-lab-1.iso 378880 (370 KiB)
-rw------- seed-kc-lab-2.iso 378880 (370 KiB)
어디를 봐야 하는가 — base.qcow2 의 virtual size 3GiB 와 disk size
335MiB 의 차이. 그리고 게스트 디스크가 20GB 로 선언됐는데 1.4GB 와 665MB
라는 것. k3s server 가 agent 보다 두 배 이상 쓰는 것은 컨트롤 플레인
바이너리와 SQLite 때문이다.
이 결과가 의미하는 것 — 20GB × 2 = 40GB 를 선언했지만 실제로는 2.1GB 를
썼다. 디스크는 병목이 아니다. df 로 본 사용량과 VM 에 선언한 크기를 같은
것으로 보면 안 된다.
스토리지 풀
virsh pool-info default
Name: default
State: running
Persistent: yes Autostart: yes
Capacity: 225.31 GiB
Allocation: 7.84 GiB
Available: 217.46 GiB
Allocation 은 풀 전체(호스트 루트 파일시스템)의 사용량이지 VM 만의
사용량이 아니다. VM 이 얼마를 쓰는지는 위 ls -l 로 본다.
4. 부팅 — cloud-init 은 얼마나 걸리나
for i in $(seq 1 30); do
ssh -o BatchMode=yes -o ConnectTimeout=4 kc-lab-edge 'cloud-init status' 2>/dev/null \
| grep -q 'status: done' && { echo "완료 (약 $((i*10))초)"; break; }
sleep 10
done
ssh kc-lab-edge 'hostname; ip -4 -br addr show enp1s0; cloud-init status'
실측 — package_update: true 에 패키지 5개(curl nftables nginx
certbot python3-certbot-dns-cloudflare)를 받는 게스트
완료 (약 50초)
kc-lab-edge
enp1s0 UP 192.168.122.10/24 metric 100
status: done
어디를 봐야 하는가 — cloud-init status 의 세 상태를 구분한다.
| 값 | 뜻 |
|---|---|
running |
아직 진행 중. 기다린다 |
done |
끝났다 |
error |
실패. cloud-init status --long 으로 어느 모듈인지 본다 |
이 결과가 의미하는 것 — SSH 가 붙는 것과 cloud-init 이 끝난 것은 다르다.
SSH 는 먼저 열리고 패키지 설치는 뒤에 이어진다. done 을 안 기다리고 다음
단계를 치면 「방금 깐 패키지가 없다」가 나온다.
5. 네트워크 — DHCP 예약의 실제 동작
virsh net-update default add ip-dhcp-host \
"<host mac='52:54:00:aa:bb:10' name='kc-lab-edge' ip='192.168.122.10'/>" \
--live --config
실측
Updated network default persistent config and live state
어디를 봐야 하는가 — persistent config 와 live state 두 마디가 다
나오는가. --live 만 주면 앞의 것이, --config 만 주면 뒤의 것이 빠진다.
한쪽만 나온 것을 성공으로 읽으면 「재부팅하니 IP 가 바뀐다」가 된다.
예약을 먼저, VM 을 나중에
이 실험대는 예약을 넣고 나서 virt-install 했고, 게스트가 첫 부팅에서
바로 .10 을 받았다.
enp1s0 UP 192.168.122.10/24 metric 100
순서가 반대면 게스트가 동적 대역(192.168.122.2–.254)에서 아무 주소나 받고,
예약이 먹으려면 리스 만료를 기다리거나 재부팅해야 한다.
리스는 예약과 별개로 남는다
virsh net-dhcp-leases default
Expiry Time MAC address IP address Hostname
2026-09-10 09:59:54 52:54:00:aa:bb:11 192.168.122.11/24 kc-lab-1
2026-09-10 09:58:46 52:54:00:aa:bb:12 192.168.122.12/24 kc-lab-2
net-dumpxml 의 예약은 줄 의도이고, net-dhcp-leases 는 실제로 준
기록이다. 둘이 다를 수 있다.
virbr0 는 게스트가 없으면 내려간다
ip -br addr show virbr0
VM 세 대가 돌 때:
virbr0 UP 192.168.122.1/24
전부 철거한 뒤:
virbr0 DOWN 192.168.122.1/24
주소는 그대로 있고 상태만 DOWN 이다. 브리지에 붙은 tap 인터페이스가
하나도 없어서다. 네트워크 정의가 사라진 것이 아니므로 VM 을 다시 띄우면
자동으로 UP 이 된다. 이걸 고장으로 오독해 virsh net-start 를 찾아
헤매지 않는다.
6. 철거 — 실제 출력 전문
가이드에는 세우는 절차만 있고 철거가 없다. 2026-09-10 에 실제로 돌린 기록이다.
게스트
for v in kc-lab-edge kc-lab-2 kc-lab-1; do
virsh destroy "$v"
virsh undefine "$v" --remove-all-storage
done
실측 (한 대분)
Domain 'kc-lab-edge' destroyed
Domain 'kc-lab-edge' has been undefined
Volume 'vda'(/var/lib/libvirt/images/kc-lab-edge.qcow2) removed.
Volume 'vdb'(/var/lib/libvirt/images/seed-kc-lab-edge.iso) removed.
어디를 봐야 하는가 — Volume 줄이 두 개 나오는가. vda(오버레이
디스크)와 vdb(시드 ISO)다. --remove-all-storage 를 빠뜨리면 도메인만
사라지고 디스크 파일이 남아 다음 virt-install 이 「이미 있다」로 실패한다.
destroy 는 종료 신호가 아니라 전원을 뽑는 것이다. 정상 종료를 원하면
virsh shutdown 을 쓰고 꺼질 때까지 기다린다.
DHCP 예약
virsh net-update default delete ip-dhcp-host \
"<host mac='52:54:00:aa:bb:10' name='kc-lab-edge' ip='192.168.122.10'/>" \
--live --config
★ 삭제할 때도 mac·name·ip 세 속성을 다 준다. 하나라도 비면 이렇게
거부된다.
error: Failed to update network default
error: XML error: Cannot use host name '' in network 'default'
zsh 에서 루프로 돌리면 이 오류를 만난다. zsh 는 따옴표 없는 변수를 단어 분리하지 않는다. bash 에서 되던
set -- $entry가 zsh 에서는$1에 문자열 전체를 넣고$2·$3을 비운다. 그래서name=""이 된다. 세 줄을 값 그대로 쓰는 편이 안전하다.
철거 후
### 남은 예약
(없음)
### dhcp 블록
<dhcp>
<range start='192.168.122.2' end='192.168.122.254'/>
</dhcp>
동적 대역만 남는 것이 정상이다.
철거 전후 비교 — 실측
| 철거 전 | 철거 후 | |
|---|---|---|
virsh list --all |
3 대 running | (없음) |
virsh vol-list default |
7 개 | base.qcow2 1 개 |
| DHCP 예약 | 3 줄 | 0 줄 |
df -h / |
11G | 7.9G |
virbr0 |
UP | DOWN |
3.1GB 가 회수됐다. 내역은 kc-lab-1 1.4GB + kc-lab-2 665MB + 시드 ISO
3개(각 370KB)이고, kc-lab-edge 의 디스크 크기는 재 두지 않았다(미측정) —
합계에서 역산하면 1GB 안팎이다.
base.qcow2 335MB 는 남긴다 — 다음 재구축의 바닥이고, 다시 받으면 몇 분이다.
7. 실측으로 드러난 함정 셋
전부 아무 오류 없이 조용히 지나가거나, 엉뚱한 곳에서 증상이 나오는 유형이다.
① cloud-init sudo 는 리스트가 아니라 문자열
ssh kc-lab-1 'cloud-init schema -c ~/chk.yaml'
리스트 형태(sudo: ['ALL=(ALL) NOPASSWD:ALL'])일 때:
Error: Cloud config schema errors: users.0: {'name': 'donghyeon', 'groups':
['sudo'], 'shell': '/bin/bash', ...} is not valid under any of the given schemas
문자열 형태(sudo: "ALL=(ALL) NOPASSWD:ALL")일 때:
Valid cloud-config: /home/donghyeon/chk.yaml
어느 키가 문제인지 안 알려 준다. users.0 블록을 통째로 찍고 「어느
스키마에도 안 맞는다」고만 한다. 그리고 리스트 형태도 부팅은 된다 —
kc-lab-1·kc-lab-2 가 그 상태로 NOPASSWD sudo 가 멀쩡히 돌았다. 검사기만
거부하는 것이라 「검사는 실패인데 왜 되지」로 헷갈린다.
게스트 cloud-init 버전: 22.4.2 (Debian 12 genericcloud).
② nginx http2 on; 은 배포판에 따라 없다
엣지 (Debian 12): nginx version: nginx/1.22.1
lab host (Arch): nginx version: nginx/1.30.4
http2 on; 은 nginx 1.25.1 이상이다. Arch 에서 쓰던 설정을 Debian 게스트로
그대로 옮기면:
[emerg] 1339#1339: unknown directive "http2" in /etc/nginx/sites-enabled/keycloak-lab:29
nginx: configuration file /etc/nginx/nginx.conf test failed
listen 443 ssl http2; 형태는 1.22 와 1.30 양쪽에서 다 돈다.
③ Debian 기본 사이트가 default_server 를 먹고 있다
Debian 계열은 /etc/nginx/sites-enabled/default 가 처음부터 붙어 있고 :80
에 default_server 로 선언돼 있다. 실험대 설정의 listen 80 default_server
와 충돌한다. 심볼릭 링크를 걸 때 같이 지운다.
sudo rm -f /etc/nginx/sites-enabled/default
Arch 는 sites-available 관례 자체가 없어서 이 함정이 없는 대신, nginx.conf
에 include /etc/nginx/sites-enabled/*; 를 직접 넣어야 한다. 배포판이 바뀌면
함정도 바뀐다.
8. 재구축할 때 무엇이 남아 있나
철거해도 남는 것과 사라지는 것을 구분해 둔다. 이걸 모르면 재구축이 「왜 이건 이미 있지」와 「왜 이건 없지」의 반복이 된다.
| 상태 | 왜 | |
|---|---|---|
base.qcow2 (335MB) |
남는다 | 다음 오버레이의 바닥 |
| 패키지 (libvirt·qemu·nginx·certbot·kubectl) | 남는다 | 재설치가 무의미 |
~/workspace/cloud/kc-lab-{1,2}.yaml |
남는다 | 키와 비밀번호가 들어 있다 |
~/.ssh/config 의 kc-lab-* 항목 |
남는다 | 재구축해도 IP 가 같다 |
libvirt default 네트워크 정의 |
남는다 | 예약만 지웠다 |
/etc/letsencrypt/ |
남긴다 (정책) | 아래 참고 |
| 게스트 디스크·시드 ISO | 사라진다 | --remove-all-storage |
| DHCP 예약 | 사라진다 | net-update delete |
| k3s·Keycloak·모든 워크로드 | 사라진다 | 게스트와 함께 |
인증서를 지우지 않는 이유
Let's Encrypt 는 같은 이름 조합에 대해 주당 중복 인증서 5장 제한이 있다. 그리고 이 실험대의 이름 셋은 tailnet 주소를 가리킨다.
dig +short auth.hyeonworks.com
100.83.212.4
100.64.0.0/10 은 CGNAT 용 예약 대역이라 공개 인터넷에서 라우팅되지
않는다. HTTP-01 검증은 Let's Encrypt 가 우리 서버로 들어오는 방식이므로,
이 주소로는 검증이 성립하지 않는다. 지우면 되살리기 전에 검증 방식 문제부터
풀어야 한다. 백업만 뜨고 파일은 남긴다.
sudo tar czf ~/letsencrypt-backup-$(date +%Y%m%d-%H%M%S).tgz -C /etc letsencrypt
복원은 반대로 한 줄이다.
sudo tar xzf ~/letsencrypt-backup-<stamp>.tgz -C /etc
미측정 — 이 실험대의 certbot 이 HTTP-01 인지 DNS-01 인지 아직 확인하지
않았다. 호스트의 sudo 가 비밀번호를 요구해 비대화식으로 읽지 못했다.
다음 한 줄이 답이다.
sudo grep -H authenticator /etc/letsencrypt/renewal/*.conf
webroot·standalone 이면 HTTP-01 이라 위 문제가 실재하고, dns-cloudflare
면 DNS-01 이라 tailnet 안에서도 재발급·갱신이 된다. 플러그인은 이미 깔려
있다 — certbot plugins 에 dns-cloudflare 가 보인다.
관련 문서
| 문서 | 무엇 |
|---|---|
guides/01-vms/ |
VM 세 대를 세우는 절차 |
guides/00-lab-host/ |
호스트 가상화 준비 |
session-lab-concepts.md |
여기 나온 개념의 정의 |
deploy/lab/host/teardown-host.sh |
호스트 계층 철거 |