Files
document-haness/docs/virtualization/source/docs/lab-virtualization.md
T
DongHyeonkaandClaude Opus 5 2109f726fe feat(pipeline): keycloak-session-store 25편·virtualization 59편을 S3→S5→S6 으로 돌린다
기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만
있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문
종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다.

SSOT 결함 둘을 고쳤다.

- kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이
  「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽
  `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다.
- virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데
  원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다.

기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은
둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더
든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다).

계약을 셋 고쳤다.

- kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/**
  28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는
  것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온
  것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수
  대조했을 때 가장 가까운 커밋도 28개가 어긋났다).
- virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고
  재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다.
- kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고
  넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다.

style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져
(실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고,
engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고
있었다(Question 기록에서 11.94 → 3.86).

verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:01:55 +09:00

17 KiB
Raw Blame History

실험대 가상화 계층 — 실측 기록

이 문서가 무엇인가

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 로 잡아 여유를 뒀다.

freeavailable(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 이 회수해 간 것으로 보인다. dommemstatactual현재 할당이지 선언한 상한이 아니다. 상한은 virsh dominfoMax 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.qcow2virtual 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 configlive 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 가 처음부터 붙어 있고 :80default_server 로 선언돼 있다. 실험대 설정의 listen 80 default_server충돌한다. 심볼릭 링크를 걸 때 같이 지운다.

sudo rm -f /etc/nginx/sites-enabled/default

Arch 는 sites-available 관례 자체가 없어서 이 함정이 없는 대신, nginx.confinclude /etc/nginx/sites-enabled/*; 를 직접 넣어야 한다. 배포판이 바뀌면 함정도 바뀐다.


8. 재구축할 때 무엇이 남아 있나

철거해도 남는 것과 사라지는 것을 구분해 둔다. 이걸 모르면 재구축이 「왜 이건 이미 있지」와 「왜 이건 없지」의 반복이 된다.

상태
base.qcow2 (335MB) 남는다 다음 오버레이의 바닥
패키지 (libvirt·qemu·nginx·certbot·kubectl) 남는다 재설치가 무의미
~/workspace/cloud/kc-lab-{1,2}.yaml 남는다 키와 비밀번호가 들어 있다
~/.ssh/configkc-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 가 우리 서버로 들어오는 방식이므로, 이 주소로는 검증이 성립하지 않는다.

지금 설정이 지우고 나면
dns-cloudflare (DNS-01) 다시 받으면 끝. 백업은 헛수고였던 것
webroot·standalone (HTTP-01) 검증 방식부터 손봐야 한다

「못 한다」가 아니라 「일이 하나 생긴다」이다. 최악의 경우라도 A 레코드를 공인 IP 로 잠깐 돌려 받거나 DNS-01 로 전환하면 된다. 다만 재구축하려고 앉은 자리에서 그 일부터 하게 된다.

어느 쪽인지 모르는 채로는 지우지 않는다는 것이 이 정책의 전부다. 백업은 tar 하나에 30초고, 답을 알고 나면 지우면 된다.

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 pluginsdns-cloudflare 가 보인다.


관련 문서

문서 무엇
guides/01-vms/ VM 세 대를 세우는 절차
guides/00-lab-host/ 호스트 가상화 준비
session-lab-concepts.md 여기 나온 개념의 정의
deploy/lab/host/teardown-host.sh 호스트 계층 철거