Files
document-haness/docs/virtualization/tech-log-studio/lab-environment-build/setup/setup-create-three-guests-with-cloud-init.md
T
DongHyeonkaandClaude Opus 5 32e39e20aa fix(setup): 실험대에서 35편을 끝까지 밟고 어긋난 명령·결과 31건을 고친다
test-server 를 비우고 다시 세운 뒤 Setup 기록 35편(virtualization 9 ·
keycloak-session-store 26)을 문서에 적힌 명령 그대로 쳤다. 어긋난 자리를
기록과 SSOT 양쪽에 실측과 함께 넣었다.

막히던 것
- 04 의 인증서 경로가 live/hyeonworks.com 이라 nginx 가 [emerg] 로 안 떴다.
  실제 계보는 live/auth.hyeonworks.com 이고 「문제가 생기면」은 진단이 거꾸로였다
- 인증서가 와일드카드가 아니다. SAN 이 auth·app1·app2 셋뿐이라 그 밖의 이름은
  TLS 에서 끊기고 curl 이 exit 60 · %{http_code} 000 을 낸다. SSOT 안에서
  두 문단이 서로 어긋나 있었다
- A-7 14번 ①이 kc-lab-1 에서 여섯 줄 다 실패하는데 마지막 date 만 「차단」을 찍는다

검사가 실패할 수 없던 자리
- B-1 의 세션 키 고르기는 앞 단계가 $KEY 를 채워 둬서 루프가 한 건도 못 맞혀도
  통과한다. KEY= 로 비우고 키마다 1/0 을 찍게 바꿨다
- k3s-agent 유닛의 sed -i 는 패턴에 $HOME 이 들어 있어 아무 줄도 안 바꾼 채 성공한다

certbot
- renew --dry-run 의 종료 코드는 성공도 0, 실패도 0, 다른 사유의 실패는 1 이다.
  본문의 renew failure(s) 로만 판정할 수 있다
- --dry-run 은 staging 서버를 쓰는데 renewal/*.conf 의 account= 는 운영 계정을
  가리킨다. 실패한 dry-run 이 staging 계정을 하나 더 만들어 다음 실행이 계속 멎는다
- 훅을 755 로 놓고 시뮬레이션이 성공해도 Running deploy-hook command 는 안 나온다.
  certbot 2.1.0 에는 --run-deploy-hooks 도 없다
- 강제 갱신은 실제로 쳤고 서빙까지 닿았다. serial 06F3E0EF…1373 → 065547…3DF1,
  notAfter Dec 3 → Dec 16, nginx worker 2629 4712 → 4745 4754

독자가 칠 수 있는 형태로
- 안 되는 형태가 번호 붙은 단계에 앉아 있던 8곳을 뒤집고, 되는 형태를 ①로 올렸다
- 랩 안에서 공개 이름을 치는 curl 65줄에 --resolve 를 붙였다. 붙인 형태를 실제로
  쳐서 문서가 적은 값과 같은지 확인했다
- 힙독·sed -i·echo >>·&&·|| 를 편집기 + 파일 리스팅 + 분할 형태로 바꿨다
- 닫는 코드펜스가 빠져 뒤 200여 줄의 블록 종류가 뒤집혀 있던 곳을 포함해 3곳을 고쳤다

관문: check_body PASS · check_prose error 0 · check_evidence 두 프로젝트 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0 · 코드펜스 전수 0건

남은 것: B-0 주입은 keycloak-pattern 저장소의 소스를 고치고 이미지를 다시 구워야
해서 안 했다(unknown).

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

37 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
name version
libvirt 12.7.0
name version
Debian GNU/Linux 12 (bookworm)
name version
cloud-init 22.4.2
final/document.md#187-단계-01-게스트-세-대
final/document.md#185-가이드-묶음이-스스로-정한-규약
final/document.md#184-이-부의-출처와-범위
9465582b5d1630eb4ae7c4e078021486919bf6b6

cloud-init 시드로 게스트 세 대를 만들고 SSH 가 키로 붙게 한다

Debian 12 클라우드 이미지 한 장 위에 오버레이로 게스트 셋을 만드는 절차다. 운영체제를 설치하지 않고 첫 부팅의 cloud-init 이 사용자와 SSH 키와 호스트명을 채운다. 끝나면 세 게스트가 192.168.122.10 부터 .12 까지를 받는다.

관계

  • lab host 에 가상화 패키지를 깔고 virsh 가 sudo 없이 돌게 만든다 앞 단계다. 여기 나오는 virt-installvirsh net-update 가 거기서 고정한 qemu:///systemdefault 네트워크 위에서 돈다.
  • 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 listsudo 없이 돈다.

되돌리는 절차는 원본 가이드 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 인가, defaultactive 인가, 그리고 virsh list --allsudo 없이 통과하는가. 세 게스트를 아직 안 만들었으면 마지막 표는 비어 있다.

이 결과가 의미하는 것 — 이 셋 가운데 하나라도 어긋난 채로 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 파일에 넣을 값 셋을 모은다

목적 — 다음 단계의 파일에 넣을 공개키 둘과 콘솔 비밀번호 하나를 손에 든다.

cat ~/.ssh/id_ed25519.pub

ssh-ed25519 ... 로 시작하는 줄이 나오면 키가 이미 있으니 ② 를 건너뛴다. No such file or directory 면 키가 없는 것이고, ② 로 만든 뒤 ① 을 다시 친다.

ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
openssl rand -base64 18

예상 결과ssh-ed25519 ... 로 시작하는 줄 둘과 base64 한 줄이 화면에 나온다. 셋 다 다음 단계에서 붙여 넣으므로 창을 닫지 않는다.

왜 필요한가 — 자리표시자를 문서에 남기지 않고 값을 찾는 명령을 함께 둔다. ④ 의 비밀번호는 cloud-init 의 plain_text_passwd 로 들어가고, 키가 안 들어갔을 때 게스트로 들어가는 유일한 통로가 된다. 이 문서에는 그 값을 싣지 않는다.

①②를 나눠 둔 까닭 — 이 실험대는 [ -f ~/.ssh/id_ed25519.pub ] || ssh-keygen … 한 줄로 쳤다(observed). 그 형태는 「있으면 두고 없으면 만든다」를 단축 평가에 접어 넣어서, 배울 것보다 [ -f … ]|| 를 먼저 읽게 만든다. 나눠 두면 키가 이미 있는지가 ① 의 출력에 그대로 보이고, 만드는 명령은 그때만 친다. 이 나눈 형태는 이 실험대에서 치지 않았다(unknown).

문제가 생기면 — ③ 만 워크스테이션에서 친다. 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]

③ 다른 게스트는 hostnamefqdn 두 줄만 kc-lab-2kc-lab-edge 로 바꿔 같은 방법으로 만든다.

예상 결과 — lab host 의 현재 디렉터리에 kc-lab-1.yamlkc-lab-2.yamlkc-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

예상 결과02. 첫 줄이 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 로 유효한 것은 다르다. 키 이름 오타(userusers)는 앞의 두 grep 도, YAML 파서도 그냥 통과한다. 오류가 나면 그 키는 조용히 무시되고, usersuser 로 잘못 쓰면 계정이 안 생기며, 증상은 역시 「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 077chmod 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-1kc-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-datameta-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 configlive 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-hostnet-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. 한 줄을 본다. 그 위 Allocating00: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-<이름>.isoCIDATA 라벨 · 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 이 회수해 간 것으로 보인다. dommemstatactual 은 현재 할당이지 선언한 상한이 아니고, 상한은 virsh dominfoMax 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 가 비밀번호를 묻지 않고 통과했는가, hostnamekc-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 statusdone 인가.

이 결과가 의미하는 것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 inforaw 라고 한다 내려받기가 끊겨 오류 페이지를 저장했다 다시 받는다
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 이 반입되지 않아 대조하지 못했다.
  • (observed) 2026-09-17 에 확인 ①·②·③ 을 그대로 쳤고 셋 다 위에 실은 대로 나왔다. 세 도메인이 running, 192.168.122.11hostnamekc-lab-1 이고 PRETTY_NAME 이 Debian 12, 엣지가 192.168.122.10/24status: done, virsh screenshot.ppm 이름으로 저장하면서 with type of image/png 라고 답하고 file 도 PNG 라고 읽는다. virsh list 의 Id 는 13·15·16 이고 줄 순서도 위와 다른데, 본문이 적은 대로 그 번호에는 뜻이 없다.
  • (observed) cloud-init 의 packages 에 certbot 은 없다. 2026-09-17 에 갈렸다. 뜬 게스트가 실제로 받은 user-data 를 열어 보니 packages: [curl, nftables] 한 줄이고, 저장소 템플릿에는 nginx · certbot · python3-certbot-dns-cloudflare 셋이 주석으로 막혀 있다. 넷 중 curlnftables 만 산다. 04 의 표에 kc-lab-edgenginx · certbot 으로 적힌 것은 그 게스트가 끝나면 맡을 역할이지 cloud-init 이 깔아 준 것이 아니다 — nginx 는 03 의 1번이, certbot 은 04 의 1번이 직접 깐다.
sudo grep -n "packages" /var/lib/cloud/instance/user-data.txt
19:packages: [curl, nftables]
  • (observed) virsh vol-info · net-dumpxml --inactive · cloud-init status --long 은 2026-09-17 에 쳤다. 볼륨 일곱의 용량과 실제 할당은 이렇다 — 선언한 크기와 디스크가 실제로 먹는 양이 크게 다르다. 오버레이라 쓴 만큼만 먹는다.
base.qcow2             Capacity=3.00 GiB     Allocation=323.25 MiB
kc-lab-1.qcow2         Capacity=20.00 GiB    Allocation=5.40 GiB
kc-lab-2.qcow2         Capacity=20.00 GiB    Allocation=3.85 GiB
kc-lab-edge.qcow2      Capacity=10.00 GiB    Allocation=316.26 MiB
seed-kc-lab-{1,2,edge}.iso   Capacity=370.00 KiB   Allocation=372.00 KiB

net-dumpxml default --inactivevirbr0 과 동적 대역 한 줄과 예약 세 줄을 낸다 — --inactive 를 붙여도 예약이 그대로 보이는 것이 --config 로 넣었다는 증거다.

  • (observed) cloud-init status --long 의 마지막 줄이 이 문서의 7번을 그대로 뒷받침한다.
status: done
boot_status_code: enabled-by-generator
last_update: Thu, 17 Sep 2026 04:24:22 +0000
detail:
DataSourceNoCloud [seed=/dev/vdb][dsmode=net]

seed=/dev/vdb 가 핵심이다. 시드를 --cloud-init 으로 붙였다면 SATA CD-ROM(sr0)이 됐을 텐데, bus=virtio 로 디스크로 붙였기 때문에 vdb 로 잡혔고 cloud-init 이 거기서 데이터소스를 찾았다. 7번이 적은 이유가 이 한 줄로 확인된다.

  • (unknown) 파일을 옮겨 검사하는 다섯 줄 형태와 virsh console 은 가이드가 적어 둔 명령이고 이 실험대가 캡처한 출력이 없다. virsh console 은 대화형이라 비대화식으로 밟지 않았다.
  • (unknown) 편집기로 kc-lab-1.yamlmeta-kc-lab-1 을 여는 형태도 이 실험대가 치지 않았다. 이 실험대는 치환 명령과 서식 출력으로 만들었고, 같은 상태에 닿는지는 다시 재지 않았다.
  • (unknown) 원본 가이드에 되돌리는 절차가 없다. 단계 03 이 지나가며 적은 한 줄 말고는 게스트와 시드 볼륨과 DHCP 예약을 걷어내는 순서가 어디에도 없다.
  • (inferred) virt-install 세 줄은 구축할 때 친 것을 옮겼고 재실행으로 검증되지 않았다.