12 KiB
실험대 운영 — 도구 · 명령 · 훈련
실험을 돌리는 데 반복해서 쓰는 것들. 개념은
session-lab-concepts.md, 계획은
session-store-lab-roadmap.md에 있다.
1. 관측 도구
htop
설치 — 세 대 모두 배포판이 다르다.
# 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를 읽는다.
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를 기본 배포하므로 바로 쓸 수 있다.
kubectl top nodes
kubectl -n header-lab top pods
htop과 보는 층이 다르다.
| 보는 것 | |
|---|---|
htop (lab host) |
VM 프로세스 = 게스트 전체 |
htop (게스트) |
게스트 안의 프로세스 |
kubectl top |
파드·노드 단위, 클러스터 관점 |
상태 점검 스크립트
./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가 회수된다.
kubectl delete ns header-lab # 실험 종료 후
모든 워크로드에 resources.limits를 반드시 건다. 안 걸면 한 파드가
게스트 메모리를 다 먹고 다른 파드까지 OOMKilled된다. 3.4GB / 2.4GB짜리
게스트에서는 현실적인 위험이다.
3. 자주 쓰는 명령
VM (lab host, LIBVIRT_DEFAULT_URI=qemu:///system)
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이다.
클러스터
kubectl get nodes -o wide
kubectl get pods -A -o wide
kubectl -n <ns> logs -f deployment/<name>
kubectl -n <ns> describe pod <pod>
kubectl -n <ns> rollout status deployment/<name>
kubectl -n <ns> rollout restart deployment/<name>
kubectl -n <ns> rollout undo deployment/<name> # 직전 버전으로
# 설정 스위치 껐다 켜기 — 실험의 기본 동작
kubectl -n <ns> set env deployment/<name> KEY=VALUE
# 임시 파드로 클러스터 안에서 테스트
kubectl -n <ns> run t --rm -i --restart=Never --image=curlimages/curl:8.11.1 -- \
curl -s http://<svc>:<port>/path
이미지 반입
k3s는 containerd를 쓰고 레지스트리가 없다. 자체 빌드 이미지는 매번 이 경로를 탄다.
# 워크스테이션에서
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이 있으면 다른 소켓을
보게 되어 "성공했는데 파드는 못 찾는" 상태가 된다.
저장소·브랜치
# 워크스테이션 — 작성·커밋
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 9절).
호스트 nginx
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를 올릴 때마다 반복된다.
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 — 설정 스위치 껐다 켜기
설정을 바꿔가며 비교하는 것이 이 실험대의 본체다.
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 — 노드를 죽였다 살리기
가장 중요하다. 장애 실험의 전제 조작이다.
# 죽이기 — 전원 차단에 해당
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를 조정할지 결정한다.
# 필요하다면
kubectl -n kube-system scale deployment/traefik --replicas=2
망가져도 된다. virt-install 한 줄로 재생성되며
(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개 항목 모두 출력 |