기록 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>
34 KiB
id, kind, slug, title, topic, topicName, project, status, studio, pinnedVersions, source, sourceRevision
| id | kind | slug | title | topic | topicName | project | status | studio | pinnedVersions | source | sourceRevision | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| f972ca27-7e18-41c1-9494-59cc6f676ae2 | SETUP | create-three-guests-with-cloud-init | cloud-init 시드로 게스트 세 대를 만들고 SSH 가 키로 붙게 한다 | lab-environment-build | 실험대 환경 구성 | virtualization | 게시 전 | https://hyeonworks.com/studio/documents/f972ca27-7e18-41c1-9494-59cc6f676ae2/edit |
|
|
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 예약이 게스트 생성보다 먼저여야 하는 까닭이 그 기준이 말하는 순서 문제다.
- 이 호스트의 가상 머신들은 설정한 메모리와 지금 잡고 있는 메모리가 얼마나 다른가 게스트마다 배정한 메모리를 여기서 정하므로 그 물음이 실제 점유량과 견줄 설정 값이 이 기록에서 나온다.
본문
읽기 전에 — 어디서 치는가
기본은 [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 이 지나가며 한 줄로 적어 두었다.
virsh undefine kc-lab-edge --remove-all-storage
그 줄은 엣지를 VM 으로 두는 까닭을 설명하면서 나오고, 세 게스트를 걷어내는 절차로 적어 둔 것이 아니다. DHCP 예약 세 줄과 풀에 올린 시드 볼륨 세 개를 어떻게 지우는지는 가이드 어디에도 없고 이 실험대도 지워 본 적이 없다. 그래서 이 절차는 되돌리기를 갖지 않는다.
세우기 전에 먼저 본다
가이드 01 에는 바꾸기 전 상태를 보는 단계가 없다. 아래 세 줄은 앞 단계의 확인을 그대로 다시 치는 것이고, 01 이 요구하는 전제가 그 셋이다(inferred).
무엇을 확인하는가 — 앞 단계가 남긴 세 값이 지금 셸에서도 그대로인지.
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 이미지 한 장을 풀 디렉터리에 둔다. 운영체제를 설치하지 않는다.
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
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 파일에 넣을 값 셋을 모은다
목적 — 다음 단계의 파일에 넣을 공개키 둘과 콘솔 비밀번호 하나를 손에 든다.
[ -f ~/.ssh/id_ed25519.pub ] || ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
cat ~/.ssh/id_ed25519.pub
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 을 걸어 두고 곧바로 자리표시자를 바꾸는 치환 명령으로 넘어가는데, 그 파일을 게스트 이름으로 복사하거나 새로 만드는 명령이 원본에도 없다. 템플릿 원문도 반입되지 않아 대조하지 못했다. 그래서 아래는 원본 본문이 실은 내용을 파일로 옮겨 적었다.
nano kc-lab-1.yaml
② __ 로 둘러싼 세 곳에 2번의 ①②③ 출력을 넣는다.
#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).
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 로 유효한지를 시드를 굽기 전에 본다.
grep -c '__' kc-lab-1.yaml
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 은 이 실험대에 없다. 게스트가 한 대라도 뜬 뒤에는 둘째 파일부터 그 검증기로 본다. 파일을 보내고, 들어가고, 검사하고, 지운다.
scp kc-lab-2.yaml donghyeon@192.168.122.11:~/
ssh donghyeon@192.168.122.11
chmod 600 ~/kc-lab-2.yaml
cloud-init schema -c ~/kc-lab-2.yaml
rm ~/kc-lab-2.yaml
exit
예상 결과 — 통과하면 한 줄이다(observed).
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).
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 스키마 검사기가 리스트 형태를 거부한다.
:::
sudo: ['ALL=(ALL) NOPASSWD:ALL'] # 거부된다
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
문제가 생기면 — 검사기는 어느 키가 문제인지 안 알려 준다. users.0 전체를 통째로 찍고 어느 스키마에도 안 맞는다고만 하므로 키를 하나씩 바꿔 가며 좁혀야 한다. 게다가 리스트 형태도 부팅은 된다 — kc-lab-1 과 kc-lab-2 가 그 상태로 NOPASSWD sudo 가 멀쩡히 돌고 있다. 검사기만 거부하므로 「검사는 실패했는데 왜 되지」로 읽히기 쉽다.
5. 시드 ISO 를 만들어 풀에 올린다
목적 — user-data 와 meta-data 를 cidata 라벨이 붙은 볼륨 하나로 묶어 libvirt 풀에 올린다.
nano meta-kc-lab-1
instance-id: kc-lab-1-20260912
local-hostname: kc-lab-1
kc-lab-1- 뒤의 20260912 는 예시 값이다. 원래 명령은 거기에 에폭 초를 넣었다. 값 자체에 뜻은 없고, cloud-init 이 이전 실행과 다른 인스턴스로 알아보게 이전 값과 겹치지 않게 둔다. 날짜든 에폭 초든 저번과 다르기만 하면 된다.
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
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
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).
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
이 단계가 게스트 생성보다 먼저다. 순서가 반대면 게스트가 동적 대역에서 아무 주소나 받아 버리고, 그 뒤에 예약을 넣어도 이미 잡은 리스가 유지된다. 되돌리려면 리스 만료를 기다리거나 게스트를 재시작해야 한다.
:::
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
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
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
virsh net-dumpxml default | grep -E "host mac|range start"
예상 결과 — 넣을 때 한 줄이 나오고(observed), 확인하면 네 줄이 나온다(observed).
Updated network default persistent config and live state
<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). 오류처럼 보이지만 이미 들어가 있다는 뜻이라 그냥 넘어가면 된다.
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 '' 로 끝난다. 값을 그대로 세 번 치는 쪽이 안전하다. 재부팅 뒤에도 남는지는 비활성 정의를 따로 본다.
virsh net-dumpxml --inactive default
7. 게스트 셋을 만든다
목적 — base 이미지 위의 오버레이 디스크와 시드 볼륨을 붙여 세 게스트를 띄운다.
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
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
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).
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 이 제 일을 했는가
무엇을 확인하는가 — 도메인 셋이 돌고 있는지, 그리고 게스트 안이 시드대로 채워졌는지.
virsh list --all
ssh donghyeon@192.168.122.11 'hostname; cat /etc/os-release | head -1'
실측(observed)
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 예약이 실제로 먹었는지, 그리고 초기화가 아직 도는 중인지 끝났는지.
ssh kc-lab-edge 'hostname; ip -4 -br addr show enp1s0; cloud-init status'
실측(observed)
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 면 어느 모듈이 실패했는지 길게 본다.
ssh kc-lab-edge 'cloud-init status --long'
확인 ③ 게스트에 못 들어갈 때는 화면을 직접 뜬다
무엇을 확인하는가 — SSH 가 막힌 원인이 시드 쪽인지 그 뒤인지.
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 은 돌았고 그 안의 사용자·키 단계에서 틀린 것이라, 콘솔로 들어가 게스트 안의 로그를 본다.
virsh console kc-lab-1
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세 줄은 구축할 때 친 것을 옮겼고 재실행으로 검증되지 않았다.