Files
keycloak-pattern/docs/guides/01-vms
DongHyeonkaandClaude Opus 5 8062cc9a19 docs(guides): bring the seven setup guides in line with the practitioner skill
The guides were written before the skill existed and a scored audit found
three gaps. Fixed by a subagent running under the skill, with measured
values, versions, IPs and quoted output declared off limits.

The big one: every step showed a command and its output, and almost none
said which line to look at or what it meant. 64 interpretation pairs added
across the seven files, weighted where the reading is hardest — 20 in the
Keycloak stage, where a Secret existing and a pod having received it are
different facts.

Only the extracting form of curl appeared. Where the reader meets a response
for the first time the guides now open with curl -I or curl -v and name the
lines worth reading; -w '%{http_code}' survives only where the code is a
value being compared — two upstream nodes against each other, or the 900-run
control loop.

Listing Secret keys went from a three-stage pipe to kubectl describe secret,
which prints the key names and their byte counts in one native command
without exposing a value.

And a tool assumption: jq and yamllint are installed on neither the lab host
nor the guests. The guides now say so where JSON is read by eye, rather than
sending the reader to install something mid-diagnosis. cloud-init schema is
on the guests and is now the guest-side check.

Also removes a stray Playwright screenshot committed at the repository root
in 919547a; the evidence copy under docs/evidence/b7a-orphan-session/ is the
one the document references.

Four things the audit left standing are recorded in the agent's report rather
than papered over — notably that 04's reload measurements are stated without
a reproduction procedure, and that 05 and 06 reference each other as
prerequisites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 17:33:31 +09:00
..

01 — VM 두 대

이 단계가 끝나면

kc-lab-1·kc-lab-2 두 게스트가 뜨고, 호스트에서 SSH 가 키로 붙는다.

전제

00 이 끝나 virsh list 가 sudo 없이 돈다.

왜 VM 두 대인가

이 실험대의 질문 전부가 「인스턴스가 둘 이상이고 요청이 어느 쪽으로 갈지 모른다」 를 전제한다. 한 대면 세션 공유도 분단도 노드 상실도 실험이 되지 않는다. 그리고 호스트에 직접 깔면 되돌릴 수가 없다 — 이 실험대는 노드를 죽이고 DB 를 crash 시키는 것이 목적이라 망가뜨릴 수 있는 층이 필요하다.


1. base 이미지를 받는다

OS 를 설치하지 않는다. 클라우드 이미지는 이미 설치가 끝난 디스크이고, 첫 부팅에서 cloud-init 이 사용자·SSH 키·호스트명을 채운다.

하기

cd /var/lib/libvirt/images
sudo curl -LO https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2
sudo mv debian-12-genericcloud-amd64.qcow2 base.qcow2

확인 — 받은 파일이 온전한 qcow2 인가

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.

#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]

세 값을 어디서 가져오나

자리표시자 셋을 실제 값으로 바꿔야 한다. 찾는 명령이 있다.

# ① 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

셋을 넣는다.

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 이 그대로 넣는다

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 에서 검사할 수 있다.

# 이 파일에는 콘솔 비밀번호가 평문으로 들어 있다 — /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'

어디를 봐야 하는가 — 마지막 한 줄. 통과하면 유효하다는 한 줄만 나오고, 아니면 Invalid cloud-config 아래에 어느 키가 왜 틀렸는지가 나열된다. 경고(deprecated)와 오류(error)는 다르다 — 경고는 지금 동작한다.

이 결과가 의미하는 것 — 오류가 나면 그 키는 조용히 무시된다. usersuser 로 잘못 쓰면 계정이 안 생기고, 증상은 역시 「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 라는 정확한 이름의 파일이 있는 볼륨을 찾는다.

하기

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

확인 — 볼륨이 풀에 올라갔고 비어 있지 않은가

virsh vol-list default

어디를 봐야 하는가seed-kc-lab-1.iso 행이 목록에 있는가. 있으면 크기까지 본다.

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 을 만든다

하기

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

kc-lab-2 는 이름·메모리(4096)·MAC(...:12)·시드만 바꾼다.

★ 시드를 --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 를 예약한다.

하기

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

--live --config 둘 다 준다. --live 만 주면 재부팅에 사라지고, --config 만 주면 지금 반영되지 않는다.

확인 — 예약이 실제로 들어갔는가

virsh net-dumpxml default | grep ip-dhcp-host -A3

실측

<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 끝 두 자리와 IP 끝 숫자가 짝이 맞는가(:11.11, :12.12). MAC 은 4번의 virt-install --network mac= 에 쓴 값과 한 글자도 다르면 안 된다. <range> 줄은 예약이 아니라 동적 대역이라, 예약 IP 가 이 안에 들어 있어도 상관없다.

이 결과가 의미하는 것 — 두 줄이 다 보이면 게스트는 재부팅해도 같은 IP 를 받는다. 한 줄만 보이면 --live --config 중 하나를 빠뜨린 것이다. net-dumpxml지금 돌고 있는 정의를 보여 주므로 여기 보이는 것은 --live 가 먹었다는 뜻이고, 재부팅 뒤에도 남는지는 virsh net-dumpxml --inactive default 로 따로 본다.

6. 붙어 본다

확인 — 떴는가, 그리고 cloud-init 이 제 일을 했는가

virsh list --all
ssh donghyeon@192.168.122.11 'hostname; cat /etc/os-release | head -1'

실측

 Id   Name       State
--------------------------
 3    kc-lab-2   running
 4    kc-lab-1   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 보다 큰 것은 아무 뜻도 없다(만들고 지운 이력일 뿐이다).


막히면

여기서 실제로 겪은 것들이다.

증상 원인 확인
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

게스트에 못 들어갈 때는 화면을 직접 뜬다.

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 은 돌았고 그 안의 사용자·키 단계에서 틀린 것이라, 이제 콘솔로 들어가 게스트 안의 로그를 본다.

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 13층에 있다.