# 01 — VM 세 대 ## 이 단계가 끝나면 `kc-lab-edge`·`kc-lab-1`·`kc-lab-2` 세 게스트가 뜨고, lab host 에서 SSH 가 키로 붙는다. ## 전제 [00](../00-lab-host/) 이 끝나 `virsh list` 가 sudo 없이 돈다. ## 왜 VM 세 대인가 **k3s 노드 두 대** — 이 실험대의 질문 전부가 **「인스턴스가 둘 이상이고 요청이 어느 쪽으로 갈지 모른다」** 를 전제한다. 한 대면 세션 공유도 분단도 노드 상실도 실험이 되지 않는다. **엣지 한 대** — nginx·인증서·certbot 이 사는 곳이다. 이것을 물리 호스트에 두면 **되돌릴 수가 없다.** 자주 고치고 자주 갈아엎는 층인데 물리 기계에 쌓이기 때문이다. VM 이면 초기화가 `virsh undefine` 한 줄이고, 물리 호스트에는 DNAT 규칙 하나와 DHCP 예약만 남는다. 자세한 이유는 [03](../03-nginx/) 의 「왜 엣지가 물리 호스트가 아니라 VM 인가」에 있다. 그리고 호스트에 직접 깔면 **되돌릴 수가 없다** — 이 실험대는 노드를 죽이고 DB 를 crash 시키는 것이 목적이라 망가뜨릴 수 있는 층이 필요하다. | 게스트 | IP | MAC 끝 | 메모리 | 무엇이 도나 | |---|---|---|---|---| | `kc-lab-edge` | 192.168.122.10 | `:10` | 1024MB | nginx · certbot | | `kc-lab-1` | 192.168.122.11 | `:11` | 5120MB | k3s server · Traefik | | `kc-lab-2` | 192.168.122.12 | `:12` | 4096MB | k3s agent · Traefik | --- ## 1. base 이미지를 받는다 OS 를 설치하지 않는다. 클라우드 이미지는 **이미 설치가 끝난 디스크**이고, 첫 부팅에서 cloud-init 이 사용자·SSH 키·호스트명을 채운다. **하기** ```bash 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 ``` **확인** — 받은 파일이 온전한 qcow2 인가 ```bash qemu-img info /var/lib/libvirt/images/base.qcow2 ``` **어디를 봐야 하는가** — 세 줄이다. `file format:` 이 `qcow2` 인가(`raw` 로 읽히면 내려받기가 중간에 끊겨 HTML 오류 페이지를 저장한 것이다), `virtual size:` 가 `disk size:` 보다 훨씬 큰가(qcow2 는 희소 파일이라 이게 정상이다), `backing file:` 줄이 **없는가**. base 는 아무것도 뒤에 두지 않는다. **이 결과가 의미하는 것** — 셋이 맞으면 이 파일을 4번에서 오버레이의 바닥으로 쓸 수 있다. 여기서 형식이 틀린 채로 진행하면 증상이 `virt-install` 이 아니라 **게스트가 부팅을 못 하는 것**으로 나타나 원인을 찾기 어려워진다. ## 2. cloud-init 을 쓴다 게스트마다 하나씩 만든다. 템플릿은 [`deploy/lab/cloud-init/kc-lab.yaml.example`](../../../deploy/lab/cloud-init/kc-lab.yaml.example). ```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] ``` ### 세 값을 어디서 가져오나 자리표시자 셋을 실제 값으로 바꿔야 한다. **찾는 명령이 있다.** ```bash # ① lab host 공개키 — 없으면 만든다 [ -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 ``` 셋을 넣는다. ```bash 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 ``` **확인** — 자리표시자가 남아 있으면 cloud-init 이 그대로 넣는다 ```bash grep -c '__' kc-lab-1.yaml # 0 이어야 한다 grep -c 'ssh-ed25519\|ssh-rsa' kc-lab-1.yaml # 2 여야 한다 python3 -c 'import yaml,sys; yaml.safe_load(open("kc-lab-1.yaml")); print("YAML OK")' ``` **어디를 봐야 하는가** — 숫자 두 개와 낱말 하나. 첫 줄이 `0` 이 아니면 `__LAB_HOST_KEY__` 같은 문자열이 남아 있는 것이고, 둘째 줄이 `2` 가 아니면 `sed` 치환 중 하나가 안 먹은 것이다. 셋째 줄은 `YAML OK` 가 찍히는가만 본다 — 찍히지 않으면 대신 파이썬 예외 줄이 나오고, 거기 적힌 `line N` 이 문제의 줄 번호다. 이 셋은 값을 뽑는 것이 아니라 **찍힌 숫자를 눈으로 비교하는** 용도라 이 형태가 맞다. **이 결과가 의미하는 것** — `0` / `2` / `YAML OK` 셋이 다 맞아야 시드를 만든다. 어긋난 채로 3번을 진행하면 **cloud-init 은 파싱에 실패해도 아무 오류를 남기지 않으므로**, 증상이 「SSH 가 안 붙는다」로만 나타나고 원인이 이 파일에 있다는 사실이 드러나지 않는다. 여기서 거르는 것이 뒤에서 30분 걸릴 일을 없앤다. > **`yamllint` 는 이 실험대에 없다.** 있으면 좋지만 없다고 설치하러 가지 > 않는다 — 위 세 줄로 충분하다. **게스트가 한 대라도 떠 있으면 한 단계 더 볼 수 있다.** YAML 로 파싱된다는 것과 **cloud-config 로 유효하다**는 것은 다르다. 키 이름 오타(`user` vs `users`)는 위 검사를 그냥 통과한다. cloud-init 자신의 스키마 검사기가 게스트에 들어 있다 — `kc-lab-2` 용 파일은 `kc-lab-1` 에서 검사할 수 있다. ```bash # 이 파일에는 콘솔 비밀번호가 평문으로 들어 있다 — /tmp 가 아니라 # 자기 홈에 600 으로 두고, 검사가 끝나면 바로 지운다 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' ``` **실측** — 통과하면 이 한 줄이다. ``` Valid cloud-config: /home/donghyeon/kc-lab-edge.yaml ``` **어디를 봐야 하는가** — 마지막 한 줄. 통과하면 유효하다는 한 줄만 나오고, 아니면 `Invalid cloud-config` 아래에 **어느 키가 왜 틀렸는지**가 나열된다. 경고(`deprecated`)와 오류(`error`)는 다르다 — 경고는 지금 동작한다. > **★ `sudo` 는 리스트가 아니라 문자열로 쓴다.** 이 실험대의 첫 두 게스트는 > 이렇게 되어 있었는데, 게스트의 cloud-init 22.4.2 스키마 검사기가 거부한다. > > ```yaml > 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 가 멀쩡히 돌고 있다). 검사기만 거부하는 것이라 > 「검사는 실패했는데 왜 되지」로 헷갈리기 쉽다. **이 결과가 의미하는 것** — 오류가 나면 그 키는 **조용히 무시된다.** `users` 를 `user` 로 잘못 쓰면 계정이 안 생기고, 증상은 역시 「SSH 가 안 붙는다」 하나뿐이다. 첫 게스트(`kc-lab-1`)를 만들 때는 검사할 게스트가 아직 없으니 위 세 줄로 가고, 둘째부터는 이 검사를 거친다. 세 가지가 의도적이다. | | 왜 | | --------------------- | -------------------------------------------------------------------------------------------------------------------- | | `NOPASSWD:ALL` | k3s 설치와 장애 주입이 비대화식으로 돌아야 한다. 비밀번호를 물으면 멈춘다 | | `plain_text_passwd` | **cloud-init 이 실패했을 때의 유일한 탈출구.** 키가 안 들어가면 SSH 가 막히므로 콘솔로 들어가 로그를 봐야 한다 | | 키 두 개 | lab host 에서 자동화가 돌고(에이전트 포워딩 없음), 워크스테이션에서는 ProxyJump 로 직접 붙는다 | > **들여쓰기는 공백만.** YAML 은 탭을 금지하고, cloud-init 은 파싱에 실패해도 > **아무 오류도 남기지 않는다.** 게스트가 `localhost` 로 뜨고 로그인이 안 되는 > 것이 유일한 증상이다. ## 3. 시드 이미지를 만든다 cloud-init 은 `cidata` 라벨이 붙고 안에 `user-data`·`meta-data` 라는 **정확한 이름**의 파일이 있는 볼륨을 찾는다. **하기** ```bash printf 'instance-id: kc-lab-1-%s\nlocal-hostname: kc-lab-1\n' "$(date +%s)" > meta-kc-lab-1 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 ``` **확인** — 볼륨이 풀에 올라갔고 비어 있지 않은가 ```bash virsh vol-list default ``` **어디를 봐야 하는가** — `seed-kc-lab-1.iso` 행이 목록에 있는가. 있으면 크기까지 본다. ```bash virsh vol-info --pool default seed-kc-lab-1.iso ``` **Capacity** 가 방금 만든 로컬 파일 크기(`stat -c%s seed-kc-lab-1.iso`)와 같아야 한다. **이 결과가 의미하는 것** — `vol-create-as` 는 **빈 볼륨을 만들 뿐**이고 내용은 `vol-upload` 가 채운다. 두 명령 중 뒤엣것을 빠뜨리면 목록에는 이름이 보이지만 안이 0 으로 채워져 있고, cloud-init 은 `cidata` 라벨을 못 찾아 조용히 끝난다 — 증상은 또 「SSH 가 안 붙는다」다. 크기가 맞으면 4번으로 간다. `instance-id` 에 타임스탬프를 넣는 이유가 있다. cloud-init 은 **인스턴스마다 한 번만** 초기화 모듈을 돌린다. id 가 같으면 이미 한 것으로 보고 건너뛰므로, user-data 를 고쳐도 반영되지 않는다. ## 4. VM 을 만든다 **하기** ```bash 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 ``` 나머지 둘은 **이름·메모리·MAC·시드**만 바꾼다. ```bash # kc-lab-2 — 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 # kc-lab-edge — 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 ``` **실측** — 엣지 생성 출력이다. ``` 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 를 실제로 쓰지 않는다. **★ 시드를 `--cloud-init` 으로 붙이지 않는다.** 그 옵션은 시드를 SATA CD-ROM 으로 붙이는데, Debian `genericcloud` 이미지는 크기를 줄이려고 물리 하드웨어 드라이버를 뺐다. **AHCI 장치가 보이지 않아** cloud-init 이 데이터소스를 못 찾고 조용히 끝난다. `bus=virtio` 로 디스크로 붙여야 한다. `--disk size=20,backing_store=...` 는 복사가 아니라 **오버레이**다. base 는 읽기 전용으로 두고 변경분만 새 파일에 쌓이므로, 20GB 를 두 개 만들어도 실제 디스크는 몇백 MB 만 쓴다. ## 5. DHCP 로 IP 를 고정한다 MAC 을 정해 두었으니 그 MAC 에 IP 를 예약한다. **하기** ```bash virsh net-update default add ip-dhcp-host \ "" \ --live --config ``` `--live --config` 둘 다 준다. `--live` 만 주면 재부팅에 사라지고, `--config` 만 주면 지금 반영되지 않는다. **확인** — 예약이 실제로 들어갔는가 ```bash virsh net-dumpxml default | grep ip-dhcp-host -A3 ``` **실측** ``` ``` **★ 예약을 먼저 넣고 VM 을 띄운다.** 순서가 반대면 게스트가 동적 대역에서 아무 주소나 받아 버리고, 예약이 먹으려면 리스 만료를 기다리거나 재부팅해야 한다. 넣을 때 나오는 한 줄은 이것이다. ``` Updated network default persistent config and live state ``` `persistent config` 와 `live state` **두 마디가 다 나와야** `--live --config` 가 제대로 먹은 것이다. **어디를 봐야 하는가** — `` 두 줄의 **MAC 끝 두 자리와 IP 끝 숫자가 짝이 맞는가**(`:11` ↔ `.11`, `:12` ↔ `.12`). MAC 은 4번의 `virt-install --network mac=` 에 쓴 값과 한 글자도 다르면 안 된다. `` 줄은 예약이 아니라 동적 대역이라, 예약 IP 가 이 안에 들어 있어도 상관없다. **이 결과가 의미하는 것** — 두 줄이 다 보이면 게스트는 재부팅해도 같은 IP 를 받는다. 한 줄만 보이면 `--live --config` 중 하나를 빠뜨린 것이다. `net-dumpxml` 은 **지금 돌고 있는 정의**를 보여 주므로 여기 보이는 것은 `--live` 가 먹었다는 뜻이고, 재부팅 뒤에도 남는지는 `virsh net-dumpxml --inactive default` 로 따로 본다. ## 6. 붙어 본다 **확인** — 떴는가, 그리고 cloud-init 이 제 일을 했는가 ```bash virsh list --all ssh donghyeon@192.168.122.11 'hostname; cat /etc/os-release | head -1' ``` **실측** ``` 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)" ``` 엣지도 같은 방법으로 본다. `cloud-init status` 까지 한 번에 친다. ```bash ssh kc-lab-edge 'hostname; ip -4 -br addr show enp1s0; cloud-init status' ``` **실측** ``` kc-lab-edge enp1s0 UP 192.168.122.10/24 metric 100 status: done ``` **어디를 봐야 하는가** — 세 줄이다. 호스트명이 `kc-lab-edge` 인가, IP 가 **예약한 `.10`** 인가, `cloud-init status` 가 `done` 인가. `running` 이면 아직 패키지를 받는 중이니 기다린다 — 이 실험대에서는 **약 50초** 걸렸다. `error` 면 `cloud-init status --long` 으로 어느 모듈이 실패했는지 본다. **어디를 봐야 하는가** — 세 가지다. ① State 가 두 대 다 `running` 인가 (`shut off` 면 아직 안 뜬 것이고, Id 칸이 `-` 로 비어 있다). ② SSH 가 **비밀번호를 묻지 않고** 통과했는가. ③ `hostname` 이 `kc-lab-1` 인가 `localhost` 인가. **이 결과가 의미하는 것** — 이 세 줄이 cloud-init 성공의 판정 기준 전부다. 호스트명이 바뀌어 있다는 것은 시드가 읽혔다는 뜻이고, 시드가 읽혔으면 같은 파일에 있던 SSH 키도 들어갔다는 뜻이다. **`localhost` 가 나오면 SSH 설정을 고치지 말고 시드부터 의심한다** — 아래 「막히면」의 화면 캡처로 간다. Id 번호가 2 보다 큰 것은 아무 뜻도 없다(만들고 지운 이력일 뿐이다). --- ## 막히면 여기서 실제로 겪은 것들이다. | 증상 | 원인 | 확인 | | ----------------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------- | | SSH`Permission denied (publickey)` · hostname 이 `localhost` | **cloud-init 이 안 돌았다.** 시드를 SATA 로 붙였거나 YAML 파싱 실패 | 아래 화면 캡처 | | user-data 를 고쳤는데 반영 안 됨 | `instance-id` 가 같아 건너뜀 | meta-data 의 id 확인 | | IP 가 매번 바뀐다 | DHCP 예약이`--config` 없이 들어감 | `net-dumpxml` | | VM 이 느리다 | KVM 미사용 | [00](../00-lab-host/) 로 | 게스트에 못 들어갈 때는 **화면을 직접 뜬다.** ```bash virsh screenshot kc-lab-1 /tmp/kc1.ppm # 확장자와 무관하게 PNG 로 저장된다 ``` **어디를 봐야 하는가** — 이미지를 열어 **로그인 프롬프트 앞의 호스트명 한 낱말**만 본다. `localhost login:` 인가 `kc-lab-1 login:` 인가. **이 결과가 의미하는 것** — `localhost` 면 cloud-init 이 아예 안 돌았다. 시드를 못 찾은 것(4번의 `bus=virtio`)이거나 YAML 파싱 실패(2번)이므로 SSH 쪽은 볼 필요가 없다. `kc-lab-1` 이면 cloud-init 은 돌았고 **그 안의 사용자·키 단계에서 틀린** 것이라, 이제 콘솔로 들어가 게스트 안의 로그를 본다. ```bash virsh console kc-lab-1 # 빠져나오려면 Ctrl+] # 게스트 안에서 sudo cloud-init status --long sudo journalctl -u cloud-init -n 50 ``` 콘솔 로그인에 쓸 비밀번호가 2번의 `plain_text_passwd` 다. **이 한 장과 이 두 줄이 「SSH 가 안 되는 이유」를 절반으로 줄인다.** --- ## 실측값 ``` kc-lab-1 vCPU 2 메모리 5120MB 192.168.122.11 kc-lab-2 vCPU 2 메모리 4096MB 192.168.122.12 게스트 OS Debian GNU/Linux 12 (bookworm) ``` > 메모리가 처음 만들 때(3584MB)와 다르다. 호스트가 12GB 뿐이라 실험을 늘리며 > 재배분했다. VM 을 다시 만들지 않고 바꾸는 방법은 > [`session-lab-concepts.md`](../../session-lab-concepts.md) 13층에 있다.