--- 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 예약이 게스트 생성보다 먼저여야 하는 까닭이 그 기준이 말하는 순서 문제다. - **이 호스트의 가상 머신들은 설정한 메모리와 지금 잡고 있는 메모리가 얼마나 다른가** 게스트마다 배정한 메모리를 여기서 정하므로 그 물음이 실제 점유량과 견줄 설정 값이 이 기록에서 나온다. ## 본문 ## 읽기 전에 — 어디서 치는가 기본은 `[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] ① 자기 공개키를 꺼내 본다" cat ~/.ssh/id_ed25519.pub ``` `ssh-ed25519 ...` 로 시작하는 줄이 나오면 키가 이미 있으니 ② 를 건너뛴다. `No such file or directory` 면 키가 없는 것이고, ② 로 만든 뒤 ① 을 다시 친다. ```bash label="[lab host] ② 키가 없을 때만 만든다" ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519 ``` ```bash label="[워크스테이션] ③ 워크스테이션 공개키를 꺼낸다" cat ~/.ssh/id_ed25519.pub ``` ```bash label="[lab host] ④ 콘솔용 비밀번호를 만든다" 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` 을 걸어 두고 곧바로 자리표시자를 바꾸는 치환 명령으로 넘어가는데, 그 파일을 게스트 이름으로 복사하거나 새로 만드는 명령이 원본에도 없다. 템플릿 원문도 반입되지 않아 대조하지 못했다. 그래서 아래는 원본 본문이 실은 내용을 파일로 옮겨 적었다. ```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 \ "" \ --live --config ``` ```bash label="[lab host] ② k3s server 게스트의 주소를 예약한다" virsh net-update default add ip-dhcp-host \ "" \ --live --config ``` ```bash label="[lab host] ③ k3s agent 게스트의 주소를 예약한다" virsh net-update default add ip-dhcp-host \ "" \ --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 ``` `persistent config` 와 `live state` 두 마디가 다 나와야 `--live --config` 가 제대로 먹었다. `` 세 줄은 MAC 끝 두 자리와 IP 끝 숫자가 짝이 맞는지 본다. `` 줄은 예약이 아니라 동적 대역이라, 예약한 주소가 그 안에 들어 있어도 상관없다. **왜 필요한가** — 아직 게스트가 없어도 예약은 들어간다. 예약은 「이 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 "" ``` **문제가 생기면** — 확인은 `` 로 한다. `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"` | `` 세 줄 · 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` 이 반입되지 않아 대조하지 못했다. - (observed) 2026-09-17 에 확인 ①·②·③ 을 그대로 쳤고 셋 다 위에 실은 대로 나왔다. 세 도메인이 `running`, `192.168.122.11` 의 `hostname` 이 `kc-lab-1` 이고 `PRETTY_NAME` 이 Debian 12, 엣지가 `192.168.122.10/24` 와 `status: 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` 셋이 **주석으로 막혀** 있다. 넷 중 `curl` 과 `nftables` 만 산다. 04 의 표에 `kc-lab-edge` 가 `nginx · certbot` 으로 적힌 것은 그 게스트가 **끝나면 맡을 역할**이지 cloud-init 이 깔아 준 것이 아니다 — nginx 는 03 의 1번이, certbot 은 04 의 1번이 직접 깐다. ```bash label="[kc-lab-edge] 실제로 받은 user-data 를 연다" sudo grep -n "packages" /var/lib/cloud/instance/user-data.txt ``` ```text label="그 출력" 19:packages: [curl, nftables] ``` - (observed) `virsh vol-info` · `net-dumpxml --inactive` · `cloud-init status --long` 은 2026-09-17 에 쳤다. 볼륨 일곱의 용량과 실제 할당은 이렇다 — **선언한 크기와 디스크가 실제로 먹는 양이 크게 다르다.** 오버레이라 쓴 만큼만 먹는다. ```text 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 --inactive` 는 `virbr0` 과 동적 대역 한 줄과 예약 세 줄을 낸다 — `--inactive` 를 붙여도 예약이 그대로 보이는 것이 `--config` 로 넣었다는 증거다. - (observed) `cloud-init status --long` 의 마지막 줄이 이 문서의 7번을 그대로 뒷받침한다. ```text 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.yaml` 과 `meta-kc-lab-1` 을 여는 형태도 이 실험대가 치지 않았다. 이 실험대는 치환 명령과 서식 출력으로 만들었고, 같은 상태에 닿는지는 다시 재지 않았다. - (unknown) 원본 가이드에 되돌리는 절차가 없다. 단계 03 이 지나가며 적은 한 줄 말고는 게스트와 시드 볼륨과 DHCP 예약을 걷어내는 순서가 어디에도 없다. - (inferred) `virt-install` 세 줄은 구축할 때 친 것을 옮겼고 재실행으로 검증되지 않았다.