기록 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>
12 KiB
kind, slug, title, topic, topicName, project, status, basisVersion, sourceRevision, source
| kind | slug | title | topic | topicName | project | status | basisVersion | sourceRevision | source | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CONCEPT | a-cloud-image-is-an-installed-disk-and-cloud-init-fills-the-blanks | 설치를 안 했는데 왜 뜨는가 — 클라우드 이미지는 설치가 끝난 디스크이고 cloud-init 이 빈칸을 채운다 | lab-environment-build | 실험대 환경 구성 | virtualization | 게시 전 | Debian 12 genericcloud 위의 cloud-init 22.4.2 · NoCloud 데이터소스 · 호스트는 QEMU 11.1.1 · libvirt 12.7.0 | 9465582b5d1630eb4ae7c4e078021486919bf6b6 |
|
설치를 안 했는데 왜 뜨는가 — 클라우드 이미지는 설치가 끝난 디스크이고 cloud-init 이 빈칸을 채운다
클라우드 이미지는 배포자가 설치를 한 번 끝내 놓은 디스크 파일이라 게스트를 만들 때 설치 단계가 없다. 대신 hostname 과 SSH 호스트키 같은 고유값을 비워 둔 채 배포하고, cloud-init 이 첫 부팅에 그 빈칸을 채운다.
관계
- cloud-init 시드로 게스트 세 대를 만들고 SSH 가 키로 붙게 한다 그 절차가 「base 이미지를 받아 오버레이로 게스트 셋을 만든다」로 시작한다. 왜 받기만 하고 설치하지 않는지를 이 글이 댄다.
- cloud-init 이 안 도는 원인은 넷인데 증상은 「SSH 가 안 붙는다」 하나였다 그 원인 넷이 전부 「시드가 안 읽혔다」로 모인다. 시드가 무엇이고 어느 단계에서 읽히는지가 여기 있다.
- qcow2 파일 안 — 매핑표와 클러스터, 그리고 항목이 0 이면 바닥에 다시 묻는다 받은 파일이 어떻게 생겼길래 3 GiB 짜리가 335MiB 인지를 그 글이 연다.
- qcow2 파일 한 장이 담는 것 — 매핑표와 데이터 클러스터가 같은 파일 안에 있고, 파일 밖을 가리키는 것은 백킹 파일 경로 하나다 받은 바닥 이미지를 다른 호스트로 들고 갈 때 무엇이 따라가는지를 그 글이 가른다.
- 가이드대로 다시 쳐서 이 실험대가 같은 상태로 서는가 게스트를 지우고 다시 만드는 비용이 낮다는 것이 이 선택의 근거인데, 그 비용을 실제로 재는 것은 그 물음이다.
본문
설치 프로그램이 만드는 것은 결국 파일 하나의 내용이다
VM 의 디스크는 호스트의 파일 하나다. kc-lab-1.qcow2 라는 파일이 게스트에게는 20GB 하드디스크로 보이고, 게스트는 그것이 파일인 줄 모르는데 QEMU 가 디스크인 척해 주기 때문이다.
그러면 「OS 를 설치한다」가 무슨 작업인지 풀어 본다.
빈 디스크
│ 설치 프로그램이 수행하는 일
├─ 파티션 테이블 작성
├─ 파일시스템 생성 (ext4, vfat …)
├─ 패키지 수천 개를 풀어 배치
├─ 부트로더 기록
└─ 초기 설정 작성
▼
"부팅 가능한 특정 바이트 배열" 상태의 디스크
설치 과정은 수단이고 목적은 마지막 줄의 상태이며, 그 상태는 파일 하나의 내용으로 남는다. Debian 과 Ubuntu 는 자기 빌드 서버에서 이 설치를 한 번 수행하고 완성된 디스크를 qcow2 파일로 떠서 공개하고, 우리는 그 파일을 내려받아 붙인다. 소스를 직접 컴파일하는 대신 이미 빌드된 바이너리를 받아 쓰는 것과 같아서, 결과물은 같고 시간만 아낀다.
그대로 복제하면 식별자가 겹치므로 일부러 비워 둔다
디스크가 바이트 단위로 같으면 안에 적힌 식별자도 같아진다.
| 값이 겹치면 | 무엇이 깨지나 |
|---|---|
| machine-id | systemd/DHCP가 두 기계를 같은 기계로 오인 |
| SSH 호스트 키 | 두 서버가 같은 신원을 주장 → MITM 탐지 무력화 |
| 파일시스템 UUID | /etc/fstab이 엉뚱한 디스크를 마운트 |
| hostname | 로그·클러스터에서 노드 구분 불가 |
그래서 클라우드 이미지는 이 값들을 비워 둔 채 배포된다.
| 배포본이 비워 두는 것 | 첫 부팅 전 상태 |
|---|---|
| hostname | 미설정 (localhost) |
| 사용자 계정 | 없음 |
| 비밀번호 | 없음 |
| SSH 호스트 키 | 없음 — 첫 부팅에 새로 생성 |
| machine-id | 비어 있음 |
cloud-init 이 이 빈칸을 첫 부팅에 채우는 장치다. user-data 라는 YAML 을 읽어 계정을 만들고 SSH 키를 등록하고 패키지를 깔고 임의의 스크립트를 실행한다.
전통적 설치 : [설치 + 개인화]를 부팅 전에 대화형으로 수행
클라우드 : [설치]는 배포자가 미리 완료
[개인화]만 첫 부팅에 cloud-init 이 자동 수행
격리는 실행 시점에 KVM/QEMU 가 만든다
「설치를 안 했으니 격리가 약한가」는 오해다. 격리는 실행 시점에 KVM/QEMU 가 만들지 설치 과정이 만들지 않는다. 게스트는 자기 커널로 부팅하고 자기 메모리 공간에서 돌며, 디스크 내용을 어떻게 얻었는지와 무관하다.
이 실험대가 cloud-init 을 고른 까닭은 재생성 비용이다
게스트에 계정과 키를 심는 방법은 셋이다.
| 계정과 키를 심는 방법 | 무엇이 드나 | 다시 만들 때 |
|---|---|---|
| ISO로 정식 설치 | VM마다 대화형 설치 | 매번 처음부터 반복 |
이미지를 미리 개조 (virt-customize, guestfish) |
libguestfs 설치 + 이미지 마운트 | 개조본을 따로 관리해야 함 |
| cloud-init | YAML 한 장 | 명령 한 줄 |
이 실험대는 virsh destroy 와 오버레이 삭제로 게스트를 반복해서 지우고 다시 만드는 것이 실험 그 자체다. 재생성 비용이 낮아야 실험이 굴러간다. 두 노드가 바이트 단위로 같은 초기 상태로 만들어져야 한다는 조건도 붙는다. 손으로 설치하면 미묘하게 달라지고 그 차이가 실험 결과를 오염시킨다.
이미지 종류도 그 축에서 고른다. Debian 은 같은 판을 여러 변종으로 배포한다.
| Debian 변종 | 어디에 쓰나 |
|---|---|
genericcloud |
가상화 환경 전용. virtio 드라이버만 담아 가볍다 → KVM에는 이걸 |
generic |
베어메탈 포함. 드라이버가 많아 더 크다 |
nocloud |
cloud-init 없이 기본 계정이 박혀 있는 변종 |
genericcloud 가 가벼운 까닭은 물리 하드웨어 드라이버를 뺐다는 데 있고, 시드를 SATA CD-ROM 으로 붙이면 게스트가 그 장치를 보지 못하는 함정도 같은 이유로 생긴다. virt-install --cloud-init 은 시드를 <target dev='sda' bus='sata'/>, 즉 SATA CD-ROM 으로 붙인다. 그래서 이 조합에서는 게스트가 시드를 아예 장치로 보지 못한다. §237 이 성공 판정을 virsh domblklist 의 장치 이름으로 잡아 둔 까닭이 여기 있다 — 시드가 vdb 로 보여야 하고, sda 로 보이면 게스트가 읽지 못한다.
게스트에게 디스크는 두 장이고, 시드는 OS 가 아니다
가장 자주 하는 오해는 시드 ISO 를 OS 이미지로 아는 것이다. 시드는 설정 데이터만 담은 370KB 짜리 별도 디스크다.
| 게스트가 보는 디스크 | 크기 | 무엇이 들었나 |
|---|---|---|
vda |
20G | ext4 루트. 여기서 부팅한다 |
vdb |
370K | LABEL=CIDATA 인 iso9660. 읽기 전용이고 마운트되지 않는다 |
vda 는 base.qcow2 위의 오버레이라 바닥 한 벌을 두 게스트가 공유하고 각자 변경분만 쌓는다. vdb 는 원본 YAML 을 구워 만든 ISO 를 풀에 올린 것이다.
kc-lab-1.yaml ──①──▶ seed-kc-lab-1.iso ──②③──▶ /var/lib/libvirt/images/seed-kc-lab-1.iso
①은 xorrisofs 가 굽는 단계이고 여기서 파일 이름이 ISO 안의 /user-data 와 /meta-data 로 바뀌는데, 두 이름이 정확해야 인식된다. ②와 ③은 virsh vol-create-as 로 자리를 잡고 virsh vol-upload 로 내용을 붓는 단계다. 홈 디렉터리가 700 이라 qemu 가 못 읽어서 풀에 둔다.
같은 내용이 세 곳에 있어서 원본만 고치면 VM 에 반영되지 않는다. 셋을 한 번에 맞추는 것이 deploy/lab/scripts/rebuild-seed.sh 다.
부팅 다섯 단계 가운데 3번이 실패하면 조용히 끝난다
1. QEMU 가 vda 에서 부팅 → Debian 커널 시작
2. cloud-init 서비스 기동 → 모든 블록 장치를 스캔
3. vdb 에서 LABEL=CIDATA 발견 → 잠깐 마운트
4. user-data / meta-data 읽기 → 사용자·hostname·sudo·패키지 적용
5. 언마운트 → SSH 로그인 가능
cloud-init 은 cidata 레이블을 가진 블록 장치를 찾지 못하면 데이터소스 없이 종료하기 때문에, 3번이 실패하면 hostname 이 localhost 로 남고 사용자가 생성되지 않는다. 오류 메시지는 어디에도 남지 않는다. virsh screenshot 으로 로그인 프롬프트만 봐도 즉시 판정할 수 있다.
user-data 파일은 #cloud-config 로 시작해야 한다. 이 첫 줄이 없으면 cloud-init 이 YAML 로 인식하지 못하고 무시하며, 증상은 「부팅은 됐는데 계정이 없다」로 나타난다.
§236 은 비상 접근 수단을 남기라는 항목 하나를 「실제로 겪은 교훈」이라고 따로 적어 두었다. ssh_pwauth: false 에 키 인증만 걸어 둔 상태로 3번이 실패하면 사용자가 생성되지 않아 키도 비밀번호도 없고, 콘솔에 붙어도 로그인할 수 없다. 실패 원인을 적어 둔 /var/log/cloud-init.log 를 읽을 방법이 그래서 사라지고, VM 을 지우고 다시 만드는 것 말고 남는 선택지가 없어진다. 콘솔 로그인용 비밀번호를 하나 넣어 두면 그 골목을 피하는데, 콘솔 로그인은 sshd 를 거치지 않으므로 ssh_pwauth: false 는 그대로 둔다.
multipass 가 감춰 주던 것이 이 단계들이다
multipass 와 virt-install 과 virsh 는 서로 배포판이 다른 도구가 아니라 계층이 다른 도구다. multipass 도 리눅스에서는 QEMU/KVM 위에서 돌고, multipass set local.driver=libvirt 로 libvirt 를 쓰게 할 수도 있다.
| multipass 가 자동으로 | 이번에 우리가 한 것 |
|---|---|
| Ubuntu 클라우드 이미지 자동 다운로드·캐시 | curl로 base.qcow2 내려받기 |
| cloud-init user-data 자동 생성 | kc-lab-1.yaml 작성 |
| 시드 ISO 생성·연결 | xorrisofs + virtio 디스크 연결 |
| SSH 키 자동 생성·주입 | ssh-keygen + ssh_authorized_keys |
| 네트워크·DHCP 구성 | virbr0 + DHCP 예약 |
multipass shell 로 즉시 접속 |
~/.ssh/config 별칭 작성 |
multipass 를 쓰지 않은 까닭은 배포판이 아니라 범위에 있다. multipass 는 Ubuntu 이미지만 공식 지원해서 Debian 게스트를 띄울 수 없다. 그리고 이 실험대는 virsh destroy 로 노드를 죽이고 NetworkPolicy 로 포트를 막고 스냅샷으로 되돌리는 저수준 제어가 실험의 본체라 관리 계층이 필요했다.
이 설명이 걸려 있는 판올림
게스트에 깔린 cloud-init 은 22.4.2 이고 데이터소스는 NoCloud 다. 판올림이 바뀌면 같은 YAML 에 대한 스키마 검사기의 판정이 달라진다.
크기 값은 두 날짜에서 왔다. 시드 ISO 370KB 와 게스트가 보는 vda 20G · vdb 370K 는 2026-09-03 실측이고, 같은 절이 base.qcow2 를 333M 로 적는다. 2026-09-10 에 다시 잰 disk size 는 335MiB 다.
파일 안이 어떻게 생겼길래 3 GiB 가 335MiB 로 앉는지는 이 글이 다루지 않는다. 시드를 굽는 세 명령의 옵션별 뜻도 마찬가지로 절차 쪽에 있다.