# 실험대 운영 — 도구 · 명령 · 훈련 실험을 돌리는 데 반복해서 쓰는 것들. 개념은 [`session-lab-concepts.md`](session-lab-concepts.md), 계획은 [`session-store-lab-roadmap.md`](session-store-lab-roadmap.md)에 있다. --- ## 1. 관측 도구 ### htop **설치** — 세 대 모두 배포판이 다르다. ```bash # lab host (Arch) sudo pacman -S htop # 게스트 (Debian) — lab host 에서 한 번에 for n in kc-lab-1 kc-lab-2; do ssh $n 'sudo apt-get update -qq && sudo apt-get install -y -qq htop' done ``` **lab host에서 htop을 읽는 법 — VM은 프로세스로 보인다** 가장 중요한 관점이다. 게스트는 **호스트 입장에서 `qemu-system-x86_64` 프로세스 하나**다. 그래서 VM의 메모리 사용량이 곧 그 프로세스의 RES다. ``` pid=4677 RSS=3765MB qemu-system-x86 ← kc-lab-1 (할당 3584M) pid=4770 RSS=2670MB qemu-system-x86 ← kc-lab-2 (할당 2560M) ``` **RSS가 할당량보다 조금 큰 이유**는 QEMU 자체의 에뮬레이션 오버헤드 (장치 모델, 버퍼)가 더해지기 때문이다. 게스트가 터치한 페이지만큼만 RSS로 잡히므로, 게스트가 메모리를 더 쓰면 RSS도 할당 상한까지 올라간다. **htop 안에서 쓸 키** | 키 | 동작 | 이 실험대에서 | |---|---|---| | `F5` | 트리 뷰 | `libvirtd` → `qemu-system` 계층 확인 | | `F4` | 필터 | `qemu`만 보기 | | `F6` | 정렬 기준 | `PERCENT_MEM`으로 정렬 | | `M` / `P` | 메모리/CPU 정렬 | | | `u` | 사용자 필터 | `libvirt-qemu`로 VM만 | | `H` | 스레드 숨김 | QEMU는 vCPU마다 스레드라 켜두면 지저분하다 | **게스트 안에서 htop을 읽을 때** 보이는 것은 `k3s`(server 또는 agent), `containerd`, `containerd-shim`, 그리고 각 파드의 `java` 프로세스다. Java 힙 상한은 컨테이너 limit의 70%(`-XX:MaxRAMPercentage=70`)이므로 512Mi limit이면 약 358Mi다. ### k9s 설치는 되어 있다. 별도 구성 없이 `~/.kube/config`를 읽는다. ```bash k9s ``` | 키 | 동작 | |---|---| | `:` | 명령 모드 — `:pods` `:svc` `:ing` `:nodes` `:events` | | `0` | 전체 네임스페이스 | | `/` | 필터 | | `l` | 로그 | | `d` | describe | | `y` | YAML | | `s` | 파드 안 셸 | | `Ctrl+d` | 파드 삭제 ← **장애 주입에 씀** | | `esc` / `q` | 뒤로 / 종료 | `~/.config/k9s/config.yaml`의 `refreshRate`를 2초로 낮추면 노드를 죽였을 때 파드 재배치가 실시간으로 보인다. ### kubectl top k3s가 metrics-server를 기본 배포하므로 바로 쓸 수 있다. ```bash kubectl top nodes kubectl -n header-lab top pods ``` **htop과 보는 층이 다르다.** | | 보는 것 | |---|---| | `htop` (lab host) | VM 프로세스 = 게스트 전체 | | `htop` (게스트) | 게스트 안의 프로세스 | | `kubectl top` | 파드·노드 단위, 클러스터 관점 | ### 상태 점검 스크립트 ```bash ./deploy/lab/scripts/verify-lab.sh # lab host 에서 ./deploy/lab/scripts/measure-proxy-headers.sh # 어디서든 ``` `verify-lab.sh`는 게스트·k3s·nginx·인증서·공개 진입점을 한 번에 확인하고 `lab is healthy`를 출력한다. **`404`가 성공 신호**다. --- ## 2. 자원 실측과 예산 **2026-09-03 기준, Keycloak 배포 전** ### 호스트 여유와 게스트 여유는 다르다 가장 오해하기 쉬운 지점이다. 호스트만 보면 절망적으로 보인다. ``` lab host 총 7628MB · 사용 7189MB · 여유 439MB ├ qemu #1 RSS 3765MB kc-lab-1 (할당 3584MB) → 상한 도달 └ qemu #2 RSS 2633MB kc-lab-2 (할당 2560MB) → 상한 도달 ``` 그런데 게스트 안을 보면 여유가 있다. ``` kc-lab-1 총 3423MB · used 1464 · buff/cache 2020 · available 1959MB kc-lab-2 총 2480MB · used 580 · buff/cache 1714 · available 1899MB ───────────────── 게스트 여유 합계 약 3.8GB ``` **왜 이런가** — QEMU의 RSS는 게스트가 **터치한 페이지**만큼이다. 게스트가 메모리를 페이지 캐시로 다 채우면 QEMU RSS도 할당 상한까지 올라간다. 지금이 그 상태다. **그래서 앞으로 워크로드를 올려도 호스트 압박은 늘지 않는다.** 게스트 안의 페이지 캐시가 밀려날 뿐이다. **QEMU RSS는 이미 천장이다.** ``` 확인 방법: ps -eo rss,args --sort=-rss | grep '[q]emu-system' # 호스트에서 본 VM ssh kc-lab-1 free -m # 게스트 안 실제 kubectl top nodes # working set ``` 세 값이 다른 것을 보는 것이 이 실험대의 메모리 감각이다. ### 배포 예산 | 워크로드 | 예상 | 배치 | |---|---|---| | Keycloak × 2 | 각 700Mi | 노드당 1개 | | PostgreSQL | 300Mi | kc-lab-1 | | Redis | 100Mi | kc-lab-2 | | BFF × 2 | 각 400Mi | 노드당 1개 | | **합계** | **약 2600Mi** | | **게스트 여유 3.8GB 중 2.6GB → 가능하다.** 다만 여기에 Prometheus/Grafana(로드맵 10번 관측성)를 얹을 여유는 없다. ### 대응 — 비용이 없는 것부터 **1. 끝난 실험은 지운다** ```bash kubectl delete ns header-lab # 파드 2개 × 150Mi 회수 ``` 증거는 `docs/evidence/`에 남아 있으므로 워크로드를 유지할 이유가 없다. **2. Keycloak 힙을 명시적으로 제한한다** Keycloak은 기본값이 넉넉해 그냥 두면 1GB를 넘긴다. ```yaml env: - name: JAVA_OPTS_KC_HEAP value: "-Xms256m -Xmx512m" resources: limits: memory: 768Mi ``` **모든 워크로드에 `resources.limits`를 반드시 건다.** 안 걸면 한 파드가 게스트 메모리를 다 먹고 다른 파드까지 OOMKilled된다. **3. 실험을 순차로 돌린다 — 동시에 다 띄우지 않는다** ``` A층(Keycloak + PostgreSQL) → 결과 기록 → 정리 ↓ B층(BFF + Redis) → 결과 기록 → 정리 ↓ 관측성(Prometheus) → 필요할 때만 ``` 절약책이 아니라 **정상적인 실험 운영 방식**이다. 동시에 띄우면 변수가 섞여서 원인 분리가 어려워진다. ### swap은 쓰지 않는다 호스트에는 8GB swap이 있지만 **게스트에는 0MB이며, 그것이 맞다.** | 이유 | | |---|---| | k3s/kubelet | 기본적으로 swap 을 거부한다 | | 성능 | 호스트 swap 으로 QEMU 페이지가 밀리면 급락한다 | | **측정 오염** | 이 실험대는 **타이밍**(refresh 경쟁, Infinispan 복제 지연)을 잰다. swap 이 끼면 측정이 통째로 무의미해진다 | ### 근본 해결 — 메모리 증설 남은 실험이 10개이고 관측성까지 하려면 증설이 가장 확실하다. ```bash sudo pacman -S dmidecode sudo dmidecode -t memory | grep -E "Maximum Capacity|Number Of Devices|Size:|Locator:|Type:|Speed:" ``` | 슬롯 상태 | 조치 | |---|---| | 2슬롯 중 1개만 사용 | 동일 규격 8GB 추가 → 16GB | | 온보드 8GB + 슬롯 1개 | 16GB 추가 → 24GB | | 2슬롯 모두 사용 | 8GB × 2 를 16GB × 2 로 교체 | i5-1135G7(Tiger Lake)은 DDR4-3200 SO-DIMM을 쓰며 최대 용량은 보드마다 다르므로 `Maximum Capacity` 값을 확인한다. **비용 대비 효과가 가장 크다** — 증설하면 Prometheus·Grafana·BFF 2 replica를 동시에 띄우고도 남는다. ## 3. 자주 쓰는 명령 ### VM (lab host, `LIBVIRT_DEFAULT_URI=qemu:///system`) ```bash virsh list --all # 상태 virsh domstate kc-lab-1 virsh domblklist kc-lab-1 # 붙은 디스크 virsh net-dhcp-leases default # 게스트 IP virsh screenshot kc-lab-1 /tmp/kc1.ppm # 화면 (PNG 로 저장됨) virsh send-key kc-lab-1 --codeset linux KEY_ENTER virsh destroy kc-lab-1 # 전원 강제 차단 = 노드 상실 virsh start kc-lab-1 # 재기동 virsh shutdown kc-lab-1 # ACPI 정상 종료 ``` **`destroy`는 파일을 지우지 않는다.** 전원 코드를 뽑는 것에 해당한다. 정의와 디스크를 지우는 것은 `undefine`이다. ### 클러스터 ```bash kubectl get nodes -o wide kubectl get pods -A -o wide kubectl -n logs -f deployment/ kubectl -n describe pod kubectl -n rollout status deployment/ kubectl -n rollout restart deployment/ kubectl -n rollout undo deployment/ # 직전 버전으로 # 설정 스위치 껐다 켜기 — 실험의 기본 동작 kubectl -n set env deployment/ KEY=VALUE # 임시 파드로 클러스터 안에서 테스트 kubectl -n run t --rm -i --restart=Never --image=curlimages/curl:8.11.1 -- \ curl -s http://:/path ``` ### 이미지 반입 k3s는 containerd를 쓰고 레지스트리가 없다. **자체 빌드 이미지는 매번 이 경로를 탄다.** ```bash # 워크스테이션에서 docker build -t keycloak-pattern-api:lab backend docker save keycloak-pattern-api:lab | ssh test-server "ssh kc-lab-1 'sudo k3s ctr images import -'" docker save keycloak-pattern-api:lab | ssh test-server "ssh kc-lab-2 'sudo k3s ctr images import -'" # 확인 ssh test-server "ssh kc-lab-1 'sudo k3s ctr images ls -q | grep keycloak-pattern'" ``` **두 노드 모두**에 넣는다. 스케줄러가 어디에 배치할지 모른다. 매니페스트는 `imagePullPolicy: Never`여야 한다. `ctr`이 아니라 **`k3s ctr`** 이다. 시스템에 별도 `ctr`이 있으면 다른 소켓을 보게 되어 "성공했는데 파드는 못 찾는" 상태가 된다. ### 저장소·브랜치 ```bash # 워크스테이션 — 작성·커밋 git add -A && git commit -m "..." && git push origin develop-keycloak-session-store # lab host — 받기만 (읽기 전용으로 운용) cd ~/workspace/keycloak-pattern && git pull # 실험별 브랜치 이동 git checkout feature/keycloak-multinode-cluster-jdbc-ping ``` **lab host의 저장소는 읽기 전용으로 쓴다.** 거기서 편집하면 드리프트가 생긴다 — nginx 설정에서 실제로 겪었다 ([`two-hop-proxy-header-contract.md`](two-hop-proxy-header-contract.md) 9절). ### 엣지 nginx (`kc-lab-edge`) **lab host 가 아니라 엣지 게스트에서 돈다.** 2026-09-10 에 엣지 계층을 물리 호스트에서 `kc-lab-edge`(192.168.122.10) 로 옮겼다 — [03](guides/03-nginx/) 의 「왜 엣지가 물리 호스트가 아니라 VM 인가」. ```bash cat deploy/lab/edge/nginx-keycloak-lab.conf \ | ssh kc-lab-edge 'sudo tee /etc/nginx/sites-available/keycloak-lab >/dev/null' ssh kc-lab-edge "sudo nginx -t && sudo systemctl reload nginx" ssh kc-lab-edge "sudo nginx -T | grep -n 'upstream\|server_name'" # 최종 병합 설정 ``` **`nginx -t`를 통과한 뒤에만 reload한다.** 깨진 설정으로 reload하면 서비스가 내려간다. `-T`(대문자)는 include까지 펼친 최종 설정을 출력하므로 "파일을 고쳤는데 반영이 안 된다" 상황의 확인 수단이다. --- ## 4. 손에 익혀야 할 훈련 개념은 알지만 직접 해보지 않은 조작들. **남은 실험 5개 중 4개가 훈련 3 위에 서 있다.** ### 훈련 1 — 이미지 반입 앞으로 BFF·token-mediator를 올릴 때마다 반복된다. ```bash docker build -t keycloak-pattern-api:lab backend docker save keycloak-pattern-api:lab | ssh test-server "ssh kc-lab-1 'sudo k3s ctr images import -'" docker save keycloak-pattern-api:lab | ssh test-server "ssh kc-lab-2 'sudo k3s ctr images import -'" ssh test-server "ssh kc-lab-1 'sudo k3s ctr images ls -q | grep keycloak-pattern'" ``` ### 훈련 2 — 설정 스위치 껐다 켜기 **설정을 바꿔가며 비교하는 것이 이 실험대의 본체**다. ```bash kubectl -n header-lab set env deployment/echo SERVER_FORWARD_HEADERS_STRATEGY=none kubectl -n header-lab rollout status deployment/echo curl -s https://app1.hyeonworks.com/api/echo | python3 -m json.tool | grep -E '"scheme"|"secure"' # → "http" / false 로 바뀐다 kubectl -n header-lab set env deployment/echo SERVER_FORWARD_HEADERS_STRATEGY=native kubectl -n header-lab rollout status deployment/echo curl -s https://app1.hyeonworks.com/api/echo | python3 -m json.tool | grep -E '"scheme"|"secure"' # → "https" / true 로 돌아온다 ``` ### 훈련 3 — 노드를 죽였다 살리기 **가장 중요하다.** 장애 실험의 전제 조작이다. ```bash # 죽이기 — 전원 차단에 해당 virsh destroy kc-lab-2 # 관찰 (NotReady 로 바뀌는 데 40초 안팎) kubectl get nodes kubectl -n header-lab get pods -o wide kubectl get events -A --sort-by=.lastTimestamp | tail -20 # 서비스가 살아있는지 curl -sI https://app1.hyeonworks.com/api/echo | head -1 # 되살리기 virsh start kc-lab-2 kubectl get nodes # Ready 복귀 kubectl -n header-lab get pods -o wide ``` **이번에 특별히 확인할 것** — 현재 **Traefik은 replica 1**이고 `kc-lab-2`에 있다. 그 노드를 죽이면 **진입점 자체가 사라지는지**, 아니면 다른 노드로 재배치되어 복구되는지 관찰한다. | 관찰 | 의미 | |---|---| | `curl`이 계속 200 | svclb가 남은 노드로 흘려보냄 + Traefik 재배치 성공 | | `curl`이 실패했다가 복구 | 재배치에 걸린 시간만큼 다운타임 | | `curl`이 계속 실패 | Traefik replica를 2로 늘려야 한다 | 이 결과에 따라 **Keycloak 배포 전에 Traefik replica를 조정할지** 결정한다. ```bash # 필요하다면 kubectl -n kube-system scale deployment/traefik --replicas=2 ``` **망가져도 된다.** `virt-install` 한 줄로 재생성되며 ([`deploy/lab/README.md`](../deploy/lab/README.md) 게스트 재생성), 그러라고 만든 실험대다. --- ## 5. 실행 주체 감사 (2026-09-03) 무엇을 직접 했고 무엇을 대행했는지의 기록. **대행한 항목은 언젠가 직접 해야 하는 것들**이다. ### 직접 수행 VM 생성 · cloud-init 작성 · SSH 키와 config · k3s server/agent 설치 · kubeconfig 구성 · 호스트 nginx 설치와 설정 · certbot DNS-01 인증서 발급 · Cloudflare DNS 레코드 · git 저장소 클론과 브랜치 · `echo.yaml` 최초 배포 · nginx `X-Forwarded-Proto` 수정 ### 대행 수행 | 작업 | 언제 다시 필요한가 | |---|---| | 이미지 빌드 → 노드 반입 (2회차 이후) | 자체 이미지를 올릴 때마다 | | `kubectl apply` (traefik HelmChartConfig, NetworkPolicy) | 모든 배포 | | `kubectl set env` / `rollout restart` | 설정 비교 실험마다 | | HelmChartConfig 재조정 대기와 파드 확인 | Traefik 설정 변경 시 | | git 브랜치 생성 · 커밋 · push | 실험마다 | | Playwright 증거 수집 | 브라우저 관점이 필요할 때마다 | | `kubectl run` 임시 파드 위조 테스트 | 클러스터 내부 검증 시 | | 설정 되돌렸다 복구 (단계별 스크린샷) | 비교 증거를 만들 때 | ### 아직 한 번도 하지 않은 것 | 항목 | 필요해지는 시점 | |---|---| | **`virsh destroy` + 재생성** | **장애 실험 전부** | | `rebuild-seed.sh` | cloud-init 을 바꿀 때 | | k9s 실사용 | 장애 중 상태 관찰 | | `kubectl delete ns` | 실험 정리, 메모리 회수 | ### 만들었지만 미검증이었던 것 → 2026-09-03 확인 완료 | 스크립트 | 결과 | |---|---| | `verify-lab.sh` | 정상 — `lab is healthy` | | `measure-proxy-headers.sh` | 정상 — 4개 항목 모두 출력 |