483 lines
16 KiB
Markdown
483 lines
16 KiB
Markdown
# 실험대 가상화 계층 — 실측 기록
|
||
|
||
## 이 문서가 무엇인가
|
||
|
||
[`docs/guides/`](guides/) 가 **「무엇을 어떤 순서로 치는가」** 를 적는다면, 이
|
||
문서는 **「그때 실제로 어떤 값이 나왔는가」** 를 적는다. 가이드에 이미 있는
|
||
절차는 반복하지 않는다.
|
||
|
||
여기 적힌 숫자는 전부 **2026-09-10 에 `test-server` 에서 실제로 돌려 받은
|
||
출력**이다. 추정값·예상값은 없다. 없는 값은 「미측정」이라고 쓴다.
|
||
|
||
| 이 문서가 답하는 것 | 가이드가 답하는 것 |
|
||
|---|---|
|
||
| 5120MB 를 줬는데 실제로 얼마를 쓰나 | 메모리를 얼마로 주나 |
|
||
| 20GB 오버레이가 디스크를 얼마나 먹나 | 오버레이를 어떻게 만드나 |
|
||
| cloud-init 이 몇 초 걸리나 | cloud-init 을 어떻게 쓰나 |
|
||
| 철거하면 무엇이 남나 | 무엇을 세우나 |
|
||
|
||
---
|
||
|
||
## 1. 측정 환경
|
||
|
||
```bash
|
||
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` 로 본다.**
|
||
|
||
### 중첩 가상화
|
||
|
||
```bash
|
||
lscpu | grep Virtualization
|
||
cat /sys/module/kvm_intel/parameters/nested
|
||
```
|
||
|
||
```
|
||
Virtualization: VT-x
|
||
Y
|
||
```
|
||
|
||
**켜져 있지만 이 실험대는 쓰지 않는다.** 일회용으로 만들려는 층(게스트)은
|
||
이미 일회용이고, 비싼 층(호스트의 인증서·DNS)은 중첩으로 해결되지 않는다.
|
||
그리고 A-6 의 지연 주입처럼 **시간을 재는 실험**에서 중첩은 측정값을 왜곡한다.
|
||
|
||
---
|
||
|
||
## 2. 자원 — 할당과 실사용은 다르다
|
||
|
||
VM 에 준 메모리는 **상한이지 점유가 아니다.** virtio-balloon 이 안 쓰는 만큼
|
||
호스트에 돌려준다.
|
||
|
||
```bash
|
||
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 를 두 개 만들어도
|
||
바닥 이미지는 한 벌이고 변경분만 쌓인다.
|
||
|
||
```bash
|
||
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 에 선언한 크기를 같은
|
||
것으로 보면 안 된다.**
|
||
|
||
### 스토리지 풀
|
||
|
||
```bash
|
||
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 은 얼마나 걸리나
|
||
|
||
```bash
|
||
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 예약의 실제 동작
|
||
|
||
```bash
|
||
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`)에서 아무 주소나 받고,
|
||
예약이 먹으려면 리스 만료를 기다리거나 재부팅해야 한다.
|
||
|
||
### 리스는 예약과 별개로 남는다
|
||
|
||
```bash
|
||
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 는 게스트가 없으면 내려간다
|
||
|
||
```bash
|
||
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 에 실제로 돌린 기록이다.
|
||
|
||
### 게스트
|
||
|
||
```bash
|
||
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 예약
|
||
|
||
```bash
|
||
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` 는 리스트가 아니라 문자열
|
||
|
||
```bash
|
||
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`
|
||
와 **충돌한다.** 심볼릭 링크를 걸 때 같이 지운다.
|
||
|
||
```bash
|
||
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 주소를 가리킨다.
|
||
|
||
```bash
|
||
dig +short auth.hyeonworks.com
|
||
```
|
||
```
|
||
100.83.212.4
|
||
```
|
||
|
||
`100.64.0.0/10` 은 CGNAT 용 예약 대역이라 **공개 인터넷에서 라우팅되지
|
||
않는다.** HTTP-01 검증은 Let's Encrypt 가 우리 서버로 들어오는 방식이므로,
|
||
이 주소로는 검증이 성립하지 않는다. **지우면 되살리기 전에 검증 방식 문제부터
|
||
풀어야 한다.** 백업만 뜨고 파일은 남긴다.
|
||
|
||
```bash
|
||
sudo tar czf ~/letsencrypt-backup-$(date +%Y%m%d-%H%M%S).tgz -C /etc letsencrypt
|
||
```
|
||
|
||
복원은 반대로 한 줄이다.
|
||
|
||
```bash
|
||
sudo tar xzf ~/letsencrypt-backup-<stamp>.tgz -C /etc
|
||
```
|
||
|
||
**미측정** — 이 실험대의 certbot 이 HTTP-01 인지 DNS-01 인지 아직 확인하지
|
||
않았다. 호스트의 `sudo` 가 비밀번호를 요구해 비대화식으로 읽지 못했다.
|
||
다음 한 줄이 답이다.
|
||
|
||
```bash
|
||
sudo grep -H authenticator /etc/letsencrypt/renewal/*.conf
|
||
```
|
||
|
||
`webroot`·`standalone` 이면 HTTP-01 이라 위 문제가 실재하고, `dns-cloudflare`
|
||
면 DNS-01 이라 tailnet 안에서도 재발급·갱신이 된다. 플러그인은 이미 깔려
|
||
있다 — `certbot plugins` 에 `dns-cloudflare` 가 보인다.
|
||
|
||
---
|
||
|
||
## 관련 문서
|
||
|
||
| 문서 | 무엇 |
|
||
|---|---|
|
||
| [`guides/01-vms/`](guides/01-vms/) | VM 세 대를 세우는 절차 |
|
||
| [`guides/00-lab-host/`](guides/00-lab-host/) | 호스트 가상화 준비 |
|
||
| [`session-lab-concepts.md`](session-lab-concepts.md) | 여기 나온 개념의 정의 |
|
||
| [`deploy/lab/host/teardown-host.sh`](../deploy/lab/host/teardown-host.sh) | 호스트 계층 철거 |
|