Files
document-haness/docs/virtualization/tech-log-studio/lab-environment-build/setup/setup-create-three-guests-with-cloud-init.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

589 lines
34 KiB
Markdown

---
id: f972ca27-7e18-41c1-9494-59cc6f676ae2
kind: SETUP
slug: create-three-guests-with-cloud-init
title: cloud-init 시드로 게스트 세 대를 만들고 SSH 가 키로 붙게 한다
topic: lab-environment-build
topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
studio: "https://hyeonworks.com/studio/documents/f972ca27-7e18-41c1-9494-59cc6f676ae2/edit"
pinnedVersions:
- name: libvirt
version: 12.7.0
- name: Debian GNU/Linux
version: 12 (bookworm)
- name: cloud-init
version: 22.4.2
source:
- final/document.md#187-단계-01-게스트-세-대
- final/document.md#185-가이드-묶음이-스스로-정한-규약
- final/document.md#184-이-부의-출처와-범위
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
---
# cloud-init 시드로 게스트 세 대를 만들고 SSH 가 키로 붙게 한다
Debian 12 클라우드 이미지 한 장 위에 오버레이로 게스트 셋을 만드는 절차다. 운영체제를 설치하지 않고 첫 부팅의 cloud-init 이 사용자와 SSH 키와 호스트명을 채운다. 끝나면 세 게스트가 192.168.122.10 부터 .12 까지를 받는다.
## 관계
- **lab host 에 가상화 패키지를 깔고 virsh 가 sudo 없이 돌게 만든다**
앞 단계다. 여기 나오는 `virt-install``virsh net-update` 가 거기서 고정한 `qemu:///system``default` 네트워크 위에서 돈다.
- **cloud-init 이 안 도는 원인은 넷인데 증상은 「SSH 가 안 붙는다」 하나였다**
이 절차에서 SSH 가 안 붙을 때 원인 넷을 가르는 방법을 그 기록이 받는다. 여기에는 세우는 순서만 있다.
- **k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다**
다음 단계이고, 여기서 고정한 192.168.122.11 과 192.168.122.12 를 그대로 쓴다.
- **qcow2 파일 한 장이 담는 것 — 매핑표와 데이터 클러스터가 같은 파일 안에 있고, 파일 밖을 가리키는 것은 백킹 파일 경로 하나다**
게스트 디스크를 base 이미지 위의 오버레이로 만들 수 있는 근거를 그 글이 설명한다.
- **단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다**
DHCP 예약이 게스트 생성보다 먼저여야 하는 까닭이 그 기준이 말하는 순서 문제다.
- **이 호스트의 가상 머신들은 설정한 메모리와 지금 잡고 있는 메모리가 얼마나 다른가**
게스트마다 배정한 메모리를 여기서 정하므로 그 물음이 실제 점유량과 견줄 설정 값이 이 기록에서 나온다.
## 본문
<!-- body:start -->
## 읽기 전에 — 어디서 치는가
기본은 `[lab host]` 다. 예외가 둘이다.
| 무엇 | 어디서 |
|---|---|
| base 이미지 · 시드 · DHCP 예약 · `virt-install` · 붙어 보기 | `[lab host]` |
| `cloud-init schema -c` | `[kc-lab-1]` — 검사기가 게스트 안에만 있다 |
| 워크스테이션 공개키를 꺼내는 한 줄 | `[워크스테이션]` |
게스트에 들어가는 일은 스키마 검사 한 번과 콘솔로 로그를 읽을 때 둘이다. 나머지는 lab host 에서 `ssh <게스트> '...'` 형태로 원격 실행한다. 게스트에는 lab host 의 개인키도 `~/.ssh/config` 도 없어서, 게스트 안에서 다른 게스트로 붙으려 하면 `Host key verification failed` 로 끝난다.
편집기를 여는 곳은 둘이다. 게스트마다의 `#cloud-config` 파일과 그 짝인 meta-data 파일이고, 나머지는 조회와 생성이라 CLI 를 그대로 쓴다.
## 이 단계가 세우는 것
가이드 01 의 「이 단계가 끝나면」은 한 줄이다.
> `kc-lab-edge`·`kc-lab-1`·`kc-lab-2` 세 게스트가 뜨고, lab host 에서 SSH 가 키로 붙는다.
| 게스트 | IP | MAC 끝 | vCPU | 메모리 | 디스크 | 무엇이 도나 |
|---|---|---|---|---|---|---|
| `kc-lab-edge` | 192.168.122.10 | `:10` | 1 | 1024MB | 10 | nginx · certbot |
| `kc-lab-1` | 192.168.122.11 | `:11` | 2 | 5120MB | 20 | k3s server · Traefik |
| `kc-lab-2` | 192.168.122.12 | `:12` | 2 | 4096MB | 20 | k3s agent · Traefik |
세 대를 세우는 까닭은 이 실험대의 질문 전부가 「인스턴스가 둘 이상이고 요청이 어느 쪽으로 갈지 모른다」를 전제하기 때문이다. 한 대면 세션 공유도 분단도 노드 상실도 실험이 되지 않는다. 엣지를 따로 둔 까닭은 그 결정을 담은 기록이 갖는다.
## 전제와 되돌리기
전제는 한 줄이다 — 앞 단계가 끝나 `virsh list``sudo` 없이 돈다.
**되돌리는 절차는 원본 가이드 01 에 없다**(unknown). 다만 01 이 만드는 것이 무엇인지는 단계 03 이 지나가며 한 줄로 적어 두었다.
```bash label="[lab host] 03 이 엣지를 왜 VM 으로 두는지 설명하며 적어 둔 한 줄"
virsh undefine kc-lab-edge --remove-all-storage
```
그 줄은 엣지를 VM 으로 두는 까닭을 설명하면서 나오고, 세 게스트를 걷어내는 절차로 적어 둔 것이 아니다. DHCP 예약 세 줄과 풀에 올린 시드 볼륨 세 개를 어떻게 지우는지는 가이드 어디에도 없고 이 실험대도 지워 본 적이 없다. 그래서 이 절차는 되돌리기를 갖지 않는다.
## 세우기 전에 먼저 본다
**가이드 01 에는 바꾸기 전 상태를 보는 단계가 없다.** 아래 세 줄은 앞 단계의 확인을 그대로 다시 치는 것이고, 01 이 요구하는 전제가 그 셋이다(inferred).
**무엇을 확인하는가** — 앞 단계가 남긴 세 값이 지금 셸에서도 그대로인지.
```bash label="[lab host] 연결 URI · 네트워크 · 도메인 목록을 차례로 본다"
virsh uri
virsh net-list --all
virsh list --all
```
**어디를 봐야 하는가** — `qemu:///system` 인가, `default` 가 `active` 인가, 그리고 `virsh list --all` 이 `sudo` 없이 통과하는가. 세 게스트를 아직 안 만들었으면 마지막 표는 비어 있다.
**이 결과가 의미하는 것** — 이 셋 가운데 하나라도 어긋난 채로 `virt-install` 을 치면 증상이 「VM 이 목록에 없다」나 「IP 를 못 받는다」로 나타나고, 원인은 이 단계가 아니라 앞 단계에 있다. 어긋난 값을 만든 곳으로 돌아간다 — 연결 URI 와 네트워크와 그룹이 각각 다른 번호다.
## 실행 절차
### 1. base 이미지를 받는다
**목적** — 게스트 셋의 디스크가 올라탈 Debian 12 genericcloud 이미지 한 장을 풀 디렉터리에 둔다. 운영체제를 설치하지 않는다.
```bash label="[lab host] ① 풀 디렉터리로 옮겨 이미지를 받는다"
cd /var/lib/libvirt/images
sudo curl -fL --output /var/lib/libvirt/images/base.qcow2 \
https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2
```
```bash label="[lab host] ② 받은 파일이 온전한 qcow2 인지 본다"
qemu-img info /var/lib/libvirt/images/base.qcow2
```
**예상 결과** — 세 줄을 본다. `file format:` 이 `qcow2` 인가, `virtual size:` 가 `disk size:` 보다 훨씬 큰가, `backing file:` 줄이 없는가. 가운데 것은 qcow2 가 희소 파일이라 그렇게 나오는 쪽이 정상이다.
**왜 필요한가** — 게스트 셋의 디스크가 전부 이 파일 위의 오버레이라, base 뒤에 또 무엇이 붙어 있으면 사슬이 한 겹 더 생긴다. base 는 아무것도 뒤에 두지 않는다.
**문제가 생기면** — 받다 끊기면 오류 페이지를 저장해서 `file format:` 이 `raw` 로 읽힌다. 형식이 틀린 채로 진행하면 증상이 `virt-install` 이 아니라 게스트가 부팅을 못 하는 것으로 나타나 원인을 찾기 어려워진다. 지우고 다시 받는다.
### 2. cloud-init 파일에 넣을 값 셋을 모은다
**목적** — 다음 단계의 파일에 넣을 공개키 둘과 콘솔 비밀번호 하나를 손에 든다.
```bash label="[lab host] ① 자기 공개키를 만들거나 꺼낸다"
[ -f ~/.ssh/id_ed25519.pub ] || ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
```
```bash label="[워크스테이션] ② 워크스테이션 공개키를 꺼낸다"
cat ~/.ssh/id_ed25519.pub
```
```bash label="[lab host] ③ 콘솔용 비밀번호를 만든다"
openssl rand -base64 18
```
**예상 결과** — `ssh-ed25519 ...` 로 시작하는 줄 둘과 base64 한 줄이 화면에 나온다. 셋 다 다음 단계에서 붙여 넣으므로 창을 닫지 않는다.
**왜 필요한가** — 자리표시자를 문서에 남기지 않고 값을 찾는 명령을 함께 둔다. ③ 의 비밀번호는 cloud-init 의 `plain_text_passwd` 로 들어가고, 키가 안 들어갔을 때 게스트로 들어가는 유일한 통로가 된다. 이 문서에는 그 값을 싣지 않는다.
**문제가 생기면** — ② 만 워크스테이션에서 친다. lab host 에서 두 번 쳐서 같은 키를 두 줄 넣으면 워크스테이션에서는 게스트에 못 붙는다.
### 3. 게스트마다 cloud-init 파일을 쓴다
**목적** — 게스트가 첫 부팅에서 읽을 `#cloud-config` 를 게스트 이름별로 하나씩 만든다.
원본 가이드에서 이 단계가 비어 있다(unknown). 템플릿으로 `deploy/lab/cloud-init/kc-lab.yaml.example` 을 걸어 두고 곧바로 자리표시자를 바꾸는 치환 명령으로 넘어가는데, 그 파일을 게스트 이름으로 복사하거나 새로 만드는 명령이 원본에도 없다. 템플릿 원문도 반입되지 않아 대조하지 못했다. 그래서 아래는 원본 본문이 실은 내용을 파일로 옮겨 적었다.
```bash label="[lab host] ① 게스트 이름으로 파일을 연다"
nano kc-lab-1.yaml
```
② `__` 로 둘러싼 세 곳에 2번의 ①②③ 출력을 넣는다.
```yaml label="kc-lab-1.yaml 에 쓸 내용"
#cloud-config
hostname: kc-lab-1
fqdn: kc-lab-1
manage_etc_hosts: true
users:
- name: donghyeon
groups: [sudo]
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
lock_passwd: false
plain_text_passwd: __CONSOLE_PW__
ssh_authorized_keys:
- __LAB_HOST_KEY__
- __WORKSTATION_KEY__
ssh_pwauth: false
package_update: true
packages: [curl, nftables]
```
③ 다른 게스트는 `hostname` 과 `fqdn` 두 줄만 `kc-lab-2` 와 `kc-lab-edge` 로 바꿔 같은 방법으로 만든다.
**예상 결과** — lab host 의 현재 디렉터리에 `kc-lab-1.yaml` 과 `kc-lab-2.yaml` 과 `kc-lab-edge.yaml` 셋이 생긴다.
**왜 필요한가** — 세 가지가 의도적이다.
| 무엇 | 왜 |
|---|---|
| `NOPASSWD:ALL` | k3s 설치와 장애 주입이 비대화식으로 돌아야 한다. 비밀번호를 물으면 멈춘다 |
| `plain_text_passwd` | cloud-init 이 실패했을 때의 유일한 탈출구. 키가 안 들어가면 SSH 가 막히므로 콘솔로 들어가 로그를 봐야 한다 |
| 키 두 개 | lab host 에서 자동화가 돌고, 워크스테이션에서는 ProxyJump 로 직접 붙는다 |
들여쓰기에는 공백만 쓴다. YAML 은 탭을 금지하고, cloud-init 은 파싱에 실패해도 아무 오류를 남기지 않아 증상이 「SSH 가 안 붙는다」 하나로만 나타난다. 편집기로 여는 까닭도 여기에 있다 — 배울 것이 `hostname:` 과 `users:` 와 `packages:` 인데 치환 명령의 구분자와 명령 치환을 먼저 읽어야 하면 시선이 그쪽으로 간다.
이 실험대는 값 셋을 한 번에 치환했다(observed).
```bash label="[lab host] 이 실험대가 실제로 친 형태"
sed -i \
-e "s|__LAB_HOST_KEY__|$(cat ~/.ssh/id_ed25519.pub)|" \
-e "s|__WORKSTATION_KEY__|<워크스테이션에서 복사해 온 값>|" \
-e "s|__CONSOLE_PW__|$(openssl rand -base64 18)|" \
kc-lab-1.yaml
```
**문제가 생기면** — `sudo:` 줄은 4번이 적은 대로 문자열 형태로 고쳐 쓴다. 그대로 두면 부팅은 되고 스키마 검사만 거절한다.
### 4. cloud-init 파일을 검사한다
**목적** — 자리표시자가 남았는지, 공개키가 둘 들어갔는지, cloud-config 로 유효한지를 시드를 굽기 전에 본다.
```bash label="[lab host] ① 자리표시자가 남았는지 센다"
grep -c '__' kc-lab-1.yaml
```
```bash label="[lab host] ② 공개키가 둘 들어갔는지 센다"
grep -c 'ssh-ed25519\|ssh-rsa' kc-lab-1.yaml
```
**예상 결과** — `0` 과 `2`. 첫 줄이 `0` 이 아니면 `__LAB_HOST_KEY__` 같은 문자열이 남아 있고, 둘째 줄이 `2` 가 아니면 키 하나가 안 들어갔다. 두 숫자가 맞아야 시드를 만든다.
첫 파일에서는 여기까지다. cloud-config 스키마까지 보는 검증기는 `cloud-init schema` 인데, 그것은 게스트 안의 cloud-init `22.4.2` 이고 첫 파일을 쓰는 시점에는 게스트가 아직 없다. lab host 에 cloud-init 이 깔려 있는지는 원본 가이드에 적혀 있지 않고 재지도 않았으며(unknown), `yamllint` 은 이 실험대에 없다. 게스트가 한 대라도 뜬 뒤에는 둘째 파일부터 그 검증기로 본다. 파일을 보내고, 들어가고, 검사하고, 지운다.
```bash label="[lab host] ③ 검사할 파일을 게스트로 보낸다"
scp kc-lab-2.yaml donghyeon@192.168.122.11:~/
```
```bash label="[lab host] ④ 그 게스트에 들어간다"
ssh donghyeon@192.168.122.11
```
```bash label="[kc-lab-1] ⑤ 권한을 좁힌다"
chmod 600 ~/kc-lab-2.yaml
```
```bash label="[kc-lab-1] ⑥ cloud-config 스키마로 검사한다"
cloud-init schema -c ~/kc-lab-2.yaml
```
```bash label="[kc-lab-1] ⑦ 검사가 끝나면 지운다"
rm ~/kc-lab-2.yaml
```
```bash label="[kc-lab-1] ⑧ lab host 로 나온다"
exit
```
**예상 결과** — 통과하면 한 줄이다(observed).
```text
Valid cloud-config: /home/donghyeon/kc-lab-edge.yaml
```
통과하면 유효하다는 한 줄만 나오고, 아니면 `Invalid cloud-config` 아래에 어느 키가 왜 틀렸는지가 나열된다. `deprecated` 경고와 `error` 는 다르다 — 경고는 지금 동작한다.
**왜 필요한가** — YAML 로 파싱되는 것과 cloud-config 로 유효한 것은 다르다. 키 이름 오타(`user` 와 `users`)는 앞의 두 `grep` 도, YAML 파서도 그냥 통과한다. 오류가 나면 그 키는 조용히 무시되고, `users` 를 `user` 로 잘못 쓰면 계정이 안 생기며, 증상은 역시 「SSH 가 안 붙는다」 하나로만 나타난다.
이 파일에는 콘솔 비밀번호가 평문으로 들어 있다. `/tmp` 가 아니라 자기 홈에 `600` 으로 두고 검사가 끝나면 바로 지운다. `scp` 가 만드는 동안에는 파일이 잠깐 기본 권한으로 놓이므로 ⑤ 를 바로 뒤에 둔다.
이 실험대는 접속과 리다이렉션과 권한 설정을 두 줄에 몰아넣었다(observed).
```bash label="[lab host] 이 실험대가 실제로 친 형태"
ssh donghyeon@192.168.122.11 'umask 077; cat > ~/kc-lab-2.yaml' < kc-lab-2.yaml
ssh donghyeon@192.168.122.11 'cloud-init schema -c ~/kc-lab-2.yaml; rm -f ~/kc-lab-2.yaml'
```
명령 수가 둘에서 다섯으로 늘고 행동 하나가 명령 하나가 된다. `umask 077` 이 `chmod 600` 으로 바뀌는 것 하나가 다르다. 이 다섯 줄 형태는 이 실험대에서 치지 않았다(unknown).
:::warning
`sudo` 는 리스트가 아니라 문자열로 쓴다. 게스트의 cloud-init 22.4.2 스키마 검사기가 리스트 형태를 거부한다.
:::
```yaml label="kc-lab-1.yaml 의 sudo 줄 — 두 형태"
sudo: ['ALL=(ALL) NOPASSWD:ALL'] # 거부된다
sudo: "ALL=(ALL) NOPASSWD:ALL" # 통과한다
```
```text
Error: Cloud config schema errors: users.0: {'name': 'donghyeon', 'groups':
['sudo'], 'shell': '/bin/bash', ...} is not valid under any of the given schemas
```
**문제가 생기면** — 검사기는 어느 키가 문제인지 안 알려 준다. `users.0` 전체를 통째로 찍고 어느 스키마에도 안 맞는다고만 하므로 키를 하나씩 바꿔 가며 좁혀야 한다. 게다가 리스트 형태도 부팅은 된다 — `kc-lab-1` 과 `kc-lab-2` 가 그 상태로 NOPASSWD sudo 가 멀쩡히 돌고 있다. 검사기만 거부하므로 「검사는 실패했는데 왜 되지」로 읽히기 쉽다.
### 5. 시드 ISO 를 만들어 풀에 올린다
**목적** — user-data 와 meta-data 를 `cidata` 라벨이 붙은 볼륨 하나로 묶어 libvirt 풀에 올린다.
```bash label="[lab host] ① meta-data 파일을 연다"
nano meta-kc-lab-1
```
```yaml label="② meta-kc-lab-1 에 쓸 내용"
instance-id: kc-lab-1-20260912
local-hostname: kc-lab-1
```
`kc-lab-1-` 뒤의 `20260912` 는 예시 값이다. 원래 명령은 거기에 에폭 초를 넣었다. 값 자체에 뜻은 없고, cloud-init 이 이전 실행과 다른 인스턴스로 알아보게 이전 값과 겹치지 않게 둔다. 날짜든 에폭 초든 저번과 다르기만 하면 된다.
```bash label="[lab host] ③ 시드 이미지를 굽는다"
xorrisofs -quiet -output seed-kc-lab-1.iso -volid CIDATA -joliet -rock \
-graft-points /user-data=kc-lab-1.yaml /meta-data=meta-kc-lab-1
```
```bash label="[lab host] ④ 빈 볼륨을 만들고 내용을 채운다"
virsh vol-create-as default seed-kc-lab-1.iso "$(stat -c%s seed-kc-lab-1.iso)" --format raw
virsh vol-upload --pool default seed-kc-lab-1.iso seed-kc-lab-1.iso
```
```bash label="[lab host] ⑤ 볼륨이 풀에 올라갔고 비어 있지 않은지 본다"
virsh vol-list default
virsh vol-info --pool default seed-kc-lab-1.iso
stat -c%s seed-kc-lab-1.iso
```
**예상 결과** — `default` 풀에 `seed-kc-lab-1.iso` 가 잡히고 `Capacity` 가 방금 만든 로컬 파일 크기와 같다.
**왜 필요한가** — 시드는 `cidata` 라벨이 붙고 안에 `user-data` 와 `meta-data` 라는 정확한 이름의 파일이 있는 볼륨이어야 한다. `vol-create-as` 는 빈 볼륨을 만들 뿐이고 내용은 `vol-upload` 가 채운다. 뒤엣것을 빠뜨리면 목록에는 이름이 보이는데 안이 0 으로 채워져 있고, cloud-init 은 `cidata` 라벨을 못 찾아 조용히 끝난다 — 증상은 또 「SSH 가 안 붙는다」다. `instance-id` 를 매번 다른 값으로 두는 것은 cloud-init 이 인스턴스마다 한 번만 초기화 모듈을 돌리기 때문이고, id 가 같으면 user-data 를 고쳐도 반영되지 않는다.
이 실험대는 meta-data 를 명령으로 만들었다(observed).
```bash label="[lab host] 이 실험대가 실제로 친 형태"
printf 'instance-id: kc-lab-1-%s\nlocal-hostname: kc-lab-1\n' "$(date +%s)" > meta-kc-lab-1
```
배울 것이 `instance-id:` 와 `local-hostname:` 두 키인데 그 형태는 서식 문자와 명령 치환을 먼저 읽게 만든다. 나머지 네 줄은 시드를 굽고 볼륨을 올리는 일이 명령 자체의 목적이라 그대로 둔다.
**문제가 생기면** — 게스트가 뜨고도 호스트명이 `localhost` 면 ④ 의 뒷줄을 빠뜨렸는지 본다.
### 6. DHCP 예약을 먼저 넣는다
**목적** — 게스트 셋의 MAC 에 주소를 못 박아 IP 가 매번 바뀌지 않게 한다.
:::warning
이 단계가 게스트 생성보다 먼저다. 순서가 반대면 게스트가 동적 대역에서 아무 주소나 받아 버리고, 그 뒤에 예약을 넣어도 이미 잡은 리스가 유지된다. 되돌리려면 리스 만료를 기다리거나 게스트를 재시작해야 한다.
:::
```bash label="[lab host] ① 엣지 게스트의 주소를 예약한다"
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
```
```bash label="[lab host] ② k3s server 게스트의 주소를 예약한다"
virsh net-update default add ip-dhcp-host \
"<host mac='52:54:00:aa:bb:11' name='kc-lab-1' ip='192.168.122.11'/>" \
--live --config
```
```bash label="[lab host] ③ k3s agent 게스트의 주소를 예약한다"
virsh net-update default add ip-dhcp-host \
"<host mac='52:54:00:aa:bb:12' name='kc-lab-2' ip='192.168.122.12'/>" \
--live --config
```
```bash label="[lab host] ④ 예약이 들어갔는지 본다"
virsh net-dumpxml default | grep -E "host mac|range start"
```
**예상 결과** — 넣을 때 한 줄이 나오고(observed), 확인하면 네 줄이 나온다(observed).
```text
Updated network default persistent config and live state
```
```text
<range start='192.168.122.2' end='192.168.122.254'/>
<host mac='52:54:00:aa:bb:11' name='kc-lab-1' ip='192.168.122.11'/>
<host mac='52:54:00:aa:bb:12' name='kc-lab-2' ip='192.168.122.12'/>
<host mac='52:54:00:aa:bb:10' name='kc-lab-edge' ip='192.168.122.10'/>
```
`persistent config` 와 `live state` 두 마디가 다 나와야 `--live --config` 가 제대로 먹었다. `<host>` 세 줄은 MAC 끝 두 자리와 IP 끝 숫자가 짝이 맞는지 본다. `<range>` 줄은 예약이 아니라 동적 대역이라, 예약한 주소가 그 안에 들어 있어도 상관없다.
**왜 필요한가** — 아직 게스트가 없어도 예약은 들어간다. 예약은 「이 MAC 이 나타나면 이 IP 를 줘라」이지 「지금 있는 게스트에게 줘라」가 아니다. MAC 은 7번의 `virt-install --network mac=` 에 쓸 값을 여기서 미리 정한다. `--live` 만 주면 재부팅에 사라지고 `--config` 만 주면 지금 반영되지 않는다.
이미 있는 예약을 또 넣으면 이렇게 거부된다(observed). 오류처럼 보이지만 이미 들어가 있다는 뜻이라 그냥 넘어가면 된다.
```text
error: Requested operation is not valid: there is an existing dhcp host entry
in network 'default' that matches "<host mac='52:54:00:aa:bb:12' ... />"
```
**문제가 생기면** — 확인은 `<host mac=...>` 로 한다. `ip-dhcp-host` 는 `net-update` 의 섹션 이름이라 XML 안에 그 문자열이 없고, 그것으로 `grep` 하면 예약이 멀쩡히 들어가 있어도 아무것도 안 나온다. 세 줄을 셸 반복문으로 돌리지도 않는다 — zsh 는 따옴표 없는 변수를 단어로 나누지 않아서 `XML error: Cannot use host name ''` 로 끝난다. 값을 그대로 세 번 치는 쪽이 안전하다. 재부팅 뒤에도 남는지는 비활성 정의를 따로 본다.
```bash label="[lab host] 재부팅 뒤에도 남는 정의를 본다"
virsh net-dumpxml --inactive default
```
### 7. 게스트 셋을 만든다
**목적** — base 이미지 위의 오버레이 디스크와 시드 볼륨을 붙여 세 게스트를 띄운다.
```bash label="[lab host] ① k3s server 게스트"
virt-install --name kc-lab-1 --memory 5120 --vcpus 2 \
--disk size=20,backing_store=/var/lib/libvirt/images/base.qcow2 \
--disk vol=default/seed-kc-lab-1.iso,device=disk,bus=virtio,readonly=on \
--network network=default,mac=52:54:00:aa:bb:11 \
--import --os-variant debian12 --noautoconsole
```
```bash label="[lab host] ② k3s agent 게스트"
virt-install --name kc-lab-2 --memory 4096 --vcpus 2 \
--disk size=20,backing_store=/var/lib/libvirt/images/base.qcow2 \
--disk vol=default/seed-kc-lab-2.iso,device=disk,bus=virtio,readonly=on \
--network network=default,mac=52:54:00:aa:bb:12 \
--import --os-variant debian12 --noautoconsole
```
```bash label="[lab host] ③ 엣지 게스트 — nginx 와 certbot 만 돌므로 훨씬 작아도 된다"
virt-install --name kc-lab-edge --memory 1024 --vcpus 1 \
--disk size=10,backing_store=/var/lib/libvirt/images/base.qcow2 \
--disk vol=default/seed-kc-lab-edge.iso,device=disk,bus=virtio,readonly=on \
--network network=default,mac=52:54:00:aa:bb:10 \
--import --os-variant debian12 --noautoconsole
```
MAC 은 6번에서 예약한 값을 한 글자도 다르지 않게 쓴다.
**예상 결과** — 엣지 생성 출력이다(observed).
```text
Starting install...
Allocating 'kc-lab-edge.qcow2' | 10 GB 00:00
Creating domain... | 00:00
Domain creation completed.
```
`Domain creation completed.` 한 줄을 본다. 그 위 `Allocating` 이 `00:00` 으로 즉시 끝나는 쪽이 정상이다 — 오버레이라 10GB 를 실제로 쓰지 않는다.
**왜 필요한가** — `virt-install` 은 VM 을 만드는 것 자체가 목적인 명령이라 옵션이 길어도 이 형태로 둔다. 여기서 준 값이 곧 게스트의 정체다 — 이름, 메모리, vCPU, 오버레이 디스크, 시드 볼륨, 예약해 둔 MAC. `--disk size=20,backing_store=...` 는 복사가 아니라 오버레이라, base 를 읽기 전용으로 두고 변경분만 새 파일에 쌓으므로 20GB 짜리를 둘 만들어도 실제 디스크는 몇백 MB 만 쓴다.
:::warning
시드를 `--cloud-init` 으로 붙이지 않는다. 그 옵션은 시드를 SATA CD-ROM 으로 붙이는데 Debian genericcloud 이미지는 크기를 줄이려고 물리 하드웨어 드라이버를 뺐고, AHCI 장치가 보이지 않아 cloud-init 이 데이터소스를 못 찾고 조용히 끝난다. `bus=virtio` 로 디스크로 붙여야 한다.
:::
**문제가 생기면** — `XML error: Cannot use host name ''` 이 나오면 6번의 예약이 빈 이름으로 들어간 것이니 그 세 줄을 값 그대로 다시 친다.
## 구성 값
게스트 셋의 배치다. 디스크는 전부 base 이미지 위의 오버레이이고 복사가 아니다.
| 게스트 | IP · MAC 끝 | vCPU · 메모리 · 디스크 | 무엇이 도나 |
|---|---|---|---|
| `kc-lab-edge` | 192.168.122.10 · `:10` | 1 · 1024MB · 10GB | nginx · certbot |
| `kc-lab-1` | 192.168.122.11 · `:11` | 2 · 5120MB · 20GB | k3s server · Traefik |
| `kc-lab-2` | 192.168.122.12 · `:12` | 2 · 4096MB · 20GB | k3s agent · Traefik |
이미지와 시드 쪽 값이다.
| 무엇 | 값 |
|---|---|
| base 이미지 | `/var/lib/libvirt/images/base.qcow2` — Debian 12 genericcloud amd64 |
| 게스트 OS | `Debian GNU/Linux 12 (bookworm)` |
| 디스크 | base 위의 오버레이(`backing_store=`). 복사가 아니다 |
| 시드 | `seed-<이름>.iso` — `CIDATA` 라벨 · `user-data`·`meta-data` · `bus=virtio` |
| cloud-init 패키지 | `[curl, nftables]` |
| 스키마 검사기 | 게스트의 cloud-init `22.4.2` |
메모리는 처음 만들 때 세 대 다 3584MB 였고, 실험을 늘리며 5120 과 4096 으로 재배분했다(observed). 세 게스트의 배정 합은 5120+4096+1024 = 10,240MB 이고, 호스트 RAM 은 11,648MiB 다. 배정 합이 더 작아서 배정률은 10240/11648 = 87.9% 이고, 배정하지 않고 남은 것이 1,408MiB 다. 그래서 이 배치는 「Guest configured memory 총량이 Host physical RAM보다 크다」는 Memory Overcommit 에 해당하지 않는다.
호스트 RAM 11,648MiB 는 2026-09-10 에 이 호스트에서 `free -m | head -2` 로 받은 `Mem:` 행의 `total` 이다. `free -m` 은 MiB 로 찍으므로, 자기 호스트에서 이 배치를 다시 잡을 때도 같은 명령으로 재고 MiB 로 읽는다.
표의 메모리 칸은 `virt-install --memory` 에 준 값, 곧 선언한 상한이다. 뒤에 `virsh dommemstat` 으로 보면 `kc-lab-2` 가 4096 이 아니라 3120 으로 나오는데 virtio-balloon 이 회수해 간 것으로 보인다. `dommemstat` 의 `actual` 은 현재 할당이지 선언한 상한이 아니고, 상한은 `virsh dominfo` 의 `Max memory` 에 있다 — 이 실험대는 그 둘을 나란히 찍어 보지 않았다. 둘을 헷갈리면 「메모리가 왜 줄었지」가 된다.
vCPU 합은 2 + 2 + 1 = 5 이고 이 호스트의 논리 코어는 8 이다. libvirt 는 vCPU 합이 논리 코어 수를 넘어도 막지 않으므로, 더 크게 잡아도 `virt-install` 은 통과한다. 여유를 둘지는 이 표에서 정한다.
## 끝났는지 판정한다
### 확인 ① 세 게스트가 떴고 cloud-init 이 제 일을 했는가
**무엇을 확인하는가** — 도메인 셋이 돌고 있는지, 그리고 게스트 안이 시드대로 채워졌는지.
```bash label="[lab host] 도메인 목록과 게스트 안을 함께 본다"
virsh list --all
ssh donghyeon@192.168.122.11 'hostname; cat /etc/os-release | head -1'
```
**실측**(observed)
```text
Id Name State
-----------------------------
2 kc-lab-1 running
4 kc-lab-2 running
5 kc-lab-edge running
kc-lab-1
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
```
**어디를 봐야 하는가** — 세 가지다. State 가 세 대 다 `running` 인가(`shut off` 면 아직 안 뜬 것이고 Id 칸이 `-` 로 비어 있다), SSH 가 비밀번호를 묻지 않고 통과했는가, `hostname` 이 `kc-lab-1` 인가 `localhost` 인가.
**이 결과가 의미하는 것** — 이 세 줄이 cloud-init 성공의 판정 기준 전부다. 호스트명이 바뀌었다는 것은 시드가 읽혔다는 뜻이고, 시드가 읽혔으면 같은 파일에 있던 SSH 키도 들어갔다는 뜻이다. `localhost` 가 나오면 SSH 설정을 고치지 말고 시드부터 의심한다. Id 번호가 2 보다 큰 것에는 아무 뜻도 없다 — 만들고 지운 이력이 그렇게 남는다.
### 확인 ② 엣지가 예약한 IP 를 받았고 cloud-init 이 끝났는가
**무엇을 확인하는가** — DHCP 예약이 실제로 먹었는지, 그리고 초기화가 아직 도는 중인지 끝났는지.
```bash label="[lab host] 호스트명 · 주소 · cloud-init 상태를 한 번에 본다"
ssh kc-lab-edge 'hostname; ip -4 -br addr show enp1s0; cloud-init status'
```
**실측**(observed)
```text
kc-lab-edge
enp1s0 UP 192.168.122.10/24 metric 100
status: done
```
**어디를 봐야 하는가** — 세 줄이다. 호스트명이 `kc-lab-edge` 인가, 주소가 예약한 `.10` 인가, `cloud-init status` 가 `done` 인가.
**이 결과가 의미하는 것** — `running` 이면 아직 패키지를 받는 중이니 기다린다. 이 실험대에서는 약 50초 걸렸다(observed). `error` 면 어느 모듈이 실패했는지 길게 본다.
```bash label="[lab host] 어느 모듈이 실패했는지 길게 본다"
ssh kc-lab-edge 'cloud-init status --long'
```
### 확인 ③ 게스트에 못 들어갈 때는 화면을 직접 뜬다
**무엇을 확인하는가** — SSH 가 막힌 원인이 시드 쪽인지 그 뒤인지.
```bash label="[lab host] 게스트 화면을 파일로 받는다"
virsh screenshot kc-lab-1 /tmp/kc1.ppm
```
확장자와 무관하게 PNG 로 저장된다.
**어디를 봐야 하는가** — 이미지를 열어 로그인 프롬프트 앞의 호스트명 한 낱말만 본다. `localhost login:` 인가 `kc-lab-1 login:` 인가.
**이 결과가 의미하는 것** — `localhost` 면 cloud-init 이 아예 안 돌았다. 시드를 못 찾았거나(7번의 `bus=virtio`) YAML 파싱에 실패한 것(3번)이므로 SSH 쪽은 볼 필요가 없다. `kc-lab-1` 이면 cloud-init 은 돌았고 그 안의 사용자·키 단계에서 틀린 것이라, 콘솔로 들어가 게스트 안의 로그를 본다.
```bash label="[lab host] 콘솔로 들어간다. 빠져나오려면 Ctrl+]"
virsh console kc-lab-1
```
```bash label="[kc-lab-1] 게스트 안에서 cloud-init 로그를 읽는다"
sudo cloud-init status --long
sudo journalctl -u cloud-init -n 50
```
콘솔 로그인에 쓰는 비밀번호가 2번 ③ 으로 만든 값이다. 이 한 장과 이 두 줄이 「SSH 가 안 되는 이유」를 절반으로 줄인다.
## 통과 조건을 한 번에 다시 본다
| 무엇 | 명령 | 통과 |
|---|---|---|
| base 이미지 | `qemu-img info /var/lib/libvirt/images/base.qcow2` | `file format: qcow2` · `backing file:` 줄이 없다 |
| 시드 볼륨 | `virsh vol-info --pool default seed-kc-lab-1.iso` | `Capacity` 가 로컬 파일 크기와 같다 |
| 예약 | `virsh net-dumpxml default \| grep -E "host mac\|range start"` | `<host>` 세 줄 · MAC 끝과 IP 끝이 짝 |
| 도메인 | `virsh list --all` | 세 대 다 `running` |
| 게스트 안 | `ssh kc-lab-edge 'hostname; cloud-init status'` | `kc-lab-edge` · `status: done` |
## 막히면
| 증상 | 원인 | 확인 |
|---|---|---|
| SSH `Permission denied (publickey)` · hostname 이 `localhost` | cloud-init 이 안 돌았다. 시드를 SATA 로 붙였거나 YAML 파싱 실패 | `virsh screenshot` |
| user-data 를 고쳤는데 반영 안 됨 | `instance-id` 가 같아 건너뜀 | meta-data 의 id 확인 |
| IP 가 매번 바뀐다 | DHCP 예약이 `--config` 없이 들어감 | `net-dumpxml` |
| `XML error: Cannot use host name ''` | zsh 가 따옴표 없는 변수를 단어 분리하지 않는다 | 세 줄을 값 그대로 친다 |
| 예약 넣을 때 `existing dhcp host entry` | 이미 들어가 있다. 오류가 아니다 | `net-dumpxml` 로 세 줄 확인 |
| 시드는 올라갔는데 cloud-init 이 안 돈다 | `vol-upload` 를 빠뜨려 볼륨이 비었다 | `virsh vol-info --pool default seed-kc-lab-1.iso` |
| `qemu-img info` 가 `raw` 라고 한다 | 내려받기가 끊겨 오류 페이지를 저장했다 | 다시 받는다 |
| VM 이 느리다 | KVM 미사용 | 앞 단계로 |
SSH 가 안 붙는 증상 넷을 갈라 보는 방법은 이 표보다 자세한 기록이 따로 있다.
## 무엇이 관측이고 무엇이 아닌가
- (observed) 게스트 세 대의 IP·MAC·vCPU·메모리, `Debian GNU/Linux 12 (bookworm)`, `cloud-init status: done`, cloud-init 대기 약 50초, 스키마 검사기의 거부 문구, `Valid cloud-config` 한 줄, 예약을 넣을 때와 다시 넣을 때의 문구, `virt-install` 의 네 줄 출력.
- (observed) 메모리는 처음 만들 때 3584MB 였고 실험을 늘리며 5120 과 4096 으로 재배분했다. 세 게스트의 배정 합 10,240MB 는 호스트 RAM 11,648MiB 보다 작고, 배정하지 않고 남은 것이 1,408MiB 다. 「Guest configured memory 총량이 Host physical RAM보다 크다」는 Memory Overcommit 에 해당하지 않는 배치다.
- (unknown) `kc-lab-1.yaml` 을 템플릿에서 어떻게 만드는지가 원본에도 없다. `kc-lab.yaml.example` 이 반입되지 않아 대조하지 못했다.
- (unknown) cloud-init 의 `packages` 에 certbot 이 들어 있었는지가 가이드 안에서 갈린다. 01 의 예시와 03 의 본문은 `curl` 과 `nftables` 뿐이라 적고, 04 는 템플릿의 `packages` 에 certbot 이 있다고 적는다.
- (unknown) 파일을 옮겨 검사하는 다섯 줄 형태, `virsh vol-list` 와 `vol-info`, `net-dumpxml --inactive`, `cloud-init status --long`, `virsh console` 은 가이드가 적어 둔 명령이고 이 실험대가 캡처한 출력이 없다.
- (unknown) 편집기로 `kc-lab-1.yaml` 과 `meta-kc-lab-1` 을 여는 형태도 이 실험대가 치지 않았다. 이 실험대는 치환 명령과 서식 출력으로 만들었고, 같은 상태에 닿는지는 다시 재지 않았다.
- (unknown) 원본 가이드에 되돌리는 절차가 없다. 단계 03 이 지나가며 적은 한 줄 말고는 게스트와 시드 볼륨과 DHCP 예약을 걷어내는 순서가 어디에도 없다.
- (inferred) `virt-install` 세 줄은 구축할 때 친 것을 옮겼고 재실행으로 검증되지 않았다.
<!-- body:end -->