# 실험대 운영 — 도구 · 명령 · 훈련 실험을 돌리는 데 반복해서 쓰는 것들. 개념은 [`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 · 사용 7227MB · 여유 400MB (+ swap 8GB) ├ qemu #1 RSS 3765MB kc-lab-1 └ qemu #2 RSS 2670MB kc-lab-2 kc-lab-1 CPU 4% 메모리 1774Mi / 3.4GB (51%) kc-lab-2 CPU 1% 메모리 1032Mi / 2.4GB (41%) echo 파드 각 150Mi ``` **호스트 여유가 400MB뿐이다.** 다만 QEMU RSS는 이미 게스트 할당 상한에 근접했으므로 **앞으로 크게 늘지 않는다.** 압박은 호스트가 아니라 **게스트 안에서** 생긴다. **다음 배포의 예상 소요** | 워크로드 | 예상 | |---|---| | Keycloak × 2 | 각 ~800Mi | | PostgreSQL | ~300Mi | | Redis | ~100Mi | | 합계 | 약 2000Mi, 게스트당 ~1000Mi | ``` kc-lab-1 1774 + 1000 ≈ 2774Mi / 3.4GB (80%) kc-lab-2 1032 + 1000 ≈ 2032Mi / 2.4GB (84%) ``` **빠듯하지만 가능하다.** 여유가 필요하면 헤더 실험이 끝난 `header-lab` 네임스페이스를 지운다 — 파드 2개 × 150Mi가 회수된다. ```bash kubectl delete ns header-lab # 실험 종료 후 ``` **모든 워크로드에 `resources.limits`를 반드시 건다.** 안 걸면 한 파드가 게스트 메모리를 다 먹고 다른 파드까지 OOMKilled된다. 3.4GB / 2.4GB짜리 게스트에서는 현실적인 위험이다. --- ## 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 ```bash sudo cp deploy/lab/host/nginx-keycloak-lab.conf /etc/nginx/sites-available/keycloak-lab sudo nginx -t && sudo systemctl reload nginx 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개 항목 모두 출력 |