Files
keycloak-pattern/docs/session-lab-operations.md

15 KiB
Raw Permalink Blame History

실험대 운영 — 도구 · 명령 · 훈련

실험을 돌리는 데 반복해서 쓰는 것들. 개념은 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 트리 뷰 libvirtdqemu-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.yamlrefreshRate를 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 · 사용 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. 끝난 실험은 지운다

kubectl delete ns header-lab      # 파드 2개 × 150Mi 회수

증거는 docs/evidence/에 남아 있으므로 워크로드를 유지할 이유가 없다.

2. Keycloak 힙을 명시적으로 제한한다

Keycloak은 기본값이 넉넉해 그냥 두면 1GB를 넘긴다.

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개이고 관측성까지 하려면 증설이 가장 확실하다.

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)

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 (kc-lab-edge)

lab host 가 아니라 엣지 게스트에서 돈다. 2026-09-10 에 엣지 계층을 물리 호스트에서 kc-lab-edge(192.168.122.10) 로 옮겼다 — 03 의 「왜 엣지가 물리 호스트가 아니라 VM 인가」.

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를 올릴 때마다 반복된다.

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개 항목 모두 출력