Files
keycloak-pattern/docs/lab-virtualization.md
T

483 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 실험대 가상화 계층 — 실측 기록
## 이 문서가 무엇인가
[`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) | 호스트 계층 철거 |