Files
keycloak-pattern/docs/guides/experiments/a7-volatile-comparison.md
T
DongHyeonkaandClaude Opus 5 6f6ab86345 docs(guides): reproduction guides for all 26 experiments
Written by subagents running under the writing-practitioner-guides skill,
one guide per experiment, 22,566 lines. Each walks a reader from baseline
capture through injection, injection verification, observation and recovery.

Section 3 carries the weight in most of them. Injection failed silently nine
times in this lab, and a failed injection looks exactly like no effect — so
the guides verify the target is actually in the intended state before
reading any result. A-4 makes virsh list the only proof because the node
reads Ready for 40 seconds after the machine is off; A-5 makes the packet
counter the sole go/no-go because a rule on the wrong node produces an empty
result that reads like a finding; A-6 quotes the run where 적용완료 was
printed between four Cannot find device "eth0" lines.

The traps the guides are built around are ones that invert a conclusion
rather than merely annoy:

  A-0   emptying the session table without a restart leaves cache entries
        that get counted as replication arriving
  A-2   dropping -o /dev/null fuses body and status into one string
  A-3   presence of "ready to accept connections" instead of its timestamp
  B-2   row count alone reads an UPDATE as nothing having happened
  B-4   tr ',' '\n' splits ["admin","editor"] so only admin is seen
  B-7   no login screen means the cookie died and SSO re-authenticated
  C-1   counting sessions without joining realm counts your own kcadm one
  D-1   kubectl exec without -i restores nothing and still exits 0
  D-4a  "ran with error output" is what success looks like

Every quoted block is copied from docs/evidence/ and marked 실측; reshaped
commands are marked 미검증 rather than passed off as measured. Where a source
document carries a ★ correction the guides follow the corrected claim — A-7's
REVOKED_TOKEN hypothesis, C-1's session count, B-2's schema attribution.

Two hazards are stated rather than smoothed over: B-6 deletes a key that
cannot be recreated, and D-1/D-4 need host sudo, which asks for a password,
so those steps say a person must type them.

Audit over all 26: 672 interpretation pairs, 486 evidence citations, 117
undo sections, and zero occurrences of the patterns the skill forbids —
no python data processing, no deprecated kubectl get endpoints, no
placeholders, no bare kcadm.

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

45 KiB
Raw Blame History

A-7 재현 가이드 — 옛 방식으로 바꿔서 A층 결론이 뒤집히는 것을 직접 본다

해설 문서: docs/experiment-a7-volatile-comparison.md · 증거 원문: docs/evidence/a7-volatile-comparison/

이 가이드가 끝나면

당신 터미널에서 이것들을 직접 본다.

보게 되는 것 어디서
로그인했는데 DB 세션 테이블이 0건인 상태 PostgreSQL OFFLINE_USER_SESSION
그런데도 교차 노드 refresh 가 200 인 것 탐침 파드
롤링 재시작 한 번에 전원 로그아웃되는 것 재시작 전 토큰으로 refresh → 400
7800 을 끊으면 이번에는 세션 공유가 깨지는 것 iptables -t raw · 교차 노드 400
DB 를 내렸는데 새 로그인이 되는 것 scale deployment/postgres --replicas=0
같은 명령이 A-1·A-8 과 정반대 답을 내는 것 위 넷 전부

전제

  • 05-keycloak · 06-observability 가 끝나 있다.
  • A-1 · A-2 · A-8 을 먼저 해 두면 좋다. 이 실험은 그 셋의 대조군이고, 기준선을 몸으로 알고 있어야 「뒤집혔다」가 보인다.
  • 명령은 kc-lab-1 에서 친다. kubectlsudo 로 쓴다.
  • kc-lab-2 에는 ssh kc-lab-2 로 붙는다. 4-3 의 iptables 는 두 노드에 각각 넣는다.
  • 터미널 두 개를 열어 두면 편하다. 하나는 관찰용, 하나는 대기용.

주의 — 이건 클러스터의 동작 모드를 바꾸는 실험이다

persistent-user-sessions 를 끈다. 전환하는 순간 기존 세션이 전부 사라지고, 되돌릴 때 또 한 번 사라진다. 빌드 옵션이라 기동 시 재빌드가 일어나 롤아웃이 평소보다 오래 걸린다(--timeout=500s 를 주는 이유다).

실험대에서만 한다. 전 구간 약 40~60분이고, 되돌리는 방법은 매 단계에 적어 두었다. 중간에 그만두려면 5. 복구 의 5-1 · 5-3 두 개면 된다.

★ 원복을 잊으면 이후 실험이 전부 오염된다. A-0 부터 A-6 까지의 결론은 전부 「persistent 기본값」 조건이다. volatile 로 둔 채 다른 실험을 하면 그 실험이 무엇을 재고 있는지 아무도 모른다.

표시 규약

표시
실측 2026-09-04 13:2213:32 KST 수집 기록의 출력 원문. 증거 파일에 그대로 있다
형태 값이 매번 달라지는 출력. 모양만 보이고 숫자는 당신 것과 다르다
미검증 손으로 치기 좋게 이 가이드에서 고친 형태. 원래 실행은 스크립트로 했다

IP·파드 이름·sid 는 당신 환경에서 다르다. 이 문서는 자리표시자(<...>)를 쓰지 않는 대신, 그 값을 뽑는 명령을 먼저 적는다. 예시로 실린 값은 전부 위 실행 기록의 실제 값이다.


0. 왜 이 실험을 하는가

A층은 여섯 개의 결론을 냈다. 그 여섯 개가 전부 하나의 전제 위에 있다.

   Keycloak 26 은 persistent-user-sessions 가 기본으로 켜져 있다
        │
        ├─ A-0  세션은 PostgreSQL 에 있다
        ├─ A-1  7800 을 끊어도 세션 공유가 안 깨진다
        ├─ A-2  DB 를 내리면 로그인이 실패한다
        └─ A-8  롤링 재시작을 해도 세션이 산다

전제를 뒤집으면 결론도 뒤집히는가. 그것이 이 실험이다.

A-1 이 본 것 인터넷 자료가 말하는 것
7800 차단 세션 공유가 안 깨진다 세션 공유가 깨진다

A-1 은 통념과 어긋난 결과를 냈고, 그 이유를 「26 이 기본값을 바꿨기 때문」이라고 설명했다. 그 설명이 맞는지는 옛 기본값으로 되돌려 같은 실험을 다시 해 봐야 판정된다. 자료가 틀린 게 아니라 버전이 다른 것이라면, 옛 설정에서는 통념이 맞아야 한다.

   persistent (KC 25+, 26 기본)      volatile (KC 24 이전)
     로그인 ─▶ PostgreSQL (진실)       로그인 ─▶ Infinispan (진실)
     조회   ─▶ 캐시 없으면 DB          조회   ─▶ 클러스터에서 찾는다
     공유   ─▶ 같은 DB 를 본다         공유   ─▶ 7800 을 통한 복제

설정 한 줄로 왼쪽에서 오른쪽으로 간다. 그 한 줄이 무엇을 바꾸는지 네 번 측정한다.


1. 기준선 — 전환하기 전에 지금이 persistent 인 것을 확인한다

시험군만 재는 측정은 측정이 아니다. 전환 후에 볼 것을 전환 전에 똑같은 명령으로 먼저 봐 둔다.

넓은 것부터 좁혀 간다.

노드 → 파드 → 지금 args → DB 세션 행 → 대조군 시험 → 이 버전에서 끌 수 있는가

1-1. 노드와 파드

확인

sudo kubectl get nodes
sudo kubectl -n keycloak-lab get pods -o wide

형태

NAME                        READY  STATUS   RESTARTS  AGE  IP           NODE
keycloak-0                  1/1    Running  0         2d   10.42.1.94   kc-lab-2
keycloak-1                  1/1    Running  0         2d   10.42.0.45   kc-lab-1
postgres-7b474b88c8-t6rrf   1/1    Running  0         5d   10.42.0.22   kc-lab-1

어디를 봐야 하는가

  • READY 가 둘 다 1/1, RESTARTS0
  • NODE 가 서로 다르다 — 같은 노드면 4-3 의 노드 간 차단이 성립하지 않는다
  • 파드 번호와 노드 번호가 어긋난다. keycloak-0kc-lab-2 에 있다. 4-3 에서 iptables 를 어느 노드에 넣을지 정할 때 이걸 헷갈리면 규칙은 걸리는데 아무 일도 안 일어난다

IP 는 변수로 잡아 둔다. 파드가 재시작되면 바뀌므로 그때마다 다시 잡는다. 이 실험은 롤아웃을 세 번 하므로 세 번 다시 잡는다.

K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
K1=$(sudo kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
echo "$K0 $K1"

실측02-a0-rerun.txt

10.42.1.94 10.42.0.45

1-2. 지금 args 가 무엇인가 — 이것이 되돌릴 값이다

확인

sudo kubectl -n keycloak-lab get statefulset keycloak \
  -o jsonpath='{.spec.template.spec.containers[0].args}' ; echo

실측06-restore-persistent.txt

["start"]

어디를 봐야 하는가["start"] 하나뿐이다. 플래그가 없다.

이 결과가 의미하는 것 — 기능 플래그를 아무것도 주지 않았으므로 26 의 기본값으로 돌고 있다. persistent-user-sessions 가 켜져 있는 상태다. 이 문자열을 적어 둔다. 5-3 에서 이 값 그대로 되돌린다.

1-3. DB 에 세션 행이 있다 — persistent 의 증거

확인

sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
  -c "select offline_flag, count(*) from offline_user_session group by offline_flag"

형태

 offline_flag | count
--------------+-------
 0            |   151

어디를 봐야 하는가offline_flag = '0' 이 온라인 세션이다. '1' 은 offline token 이고 이 실험과 무관하다.

이 결과가 의미하는 것 — 로그인한 세션이 DB 테이블에 행으로 있다. 전환 후 이 자리가 (0 rows) 가 되는 것이 이 실험의 첫 판정이다.

숫자는 당신 환경에서 다르다. 관리 API 호출도 세션을 만들기 때문에 개수에는 노이즈가 있다. 여기서 중요한 것은 0 이 아니라는 것뿐이다.

1-4. 대조군 — 교차 노드 refresh 가 지금은 되는 것

이 절을 건너뛰면 뒤의 400 이 아무 의미가 없다.

Keycloak 컨테이너에는 curlwget 도 없다(exit 127). 탐침 파드를 띄운다. 이 파드는 실험 내내 살려 둔다 — 롤링 재시작을 넘어 토큰을 들고 있어야 하기 때문이다.

하기

sudo kubectl -n keycloak-lab run a7-probe --image=curlimages/curl:8.11.1 \
  --restart=Never \
  --env="K0=$K0" --env="K1=$K1" \
  --env="PW=$(sudo kubectl -n keycloak-lab get secret keycloak-lab-secrets \
              -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)" \
  --command -- sleep 7200
sudo kubectl -n keycloak-lab wait --for=condition=Ready pod/a7-probe --timeout=120s

되돌리기

sudo kubectl -n keycloak-lab delete pod a7-probe --ignore-not-found

비밀번호를 화면에 찍지 않는다. 명령 치환으로 넘기므로 값은 터미널에도 셸 히스토리에도 남지 않는다. 존재와 길이만 확인하고 싶으면:

sudo kubectl -n keycloak-lab get secret keycloak-lab-secrets \
  -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c

실측19

확인 — 환경변수가 들어갔나

sudo kubectl -n keycloak-lab exec a7-probe -- sh -c 'echo "K0=$K0 K1=$K1 PW길이=${#PW}"'

형태

K0=10.42.1.94 K1=10.42.0.45 PW길이=19

PW길이=0 이면 --env 가 빈 값을 받은 것이다. 파드를 지우고 다시 띄운다.

하기keycloak-0 에서 로그인한다. 응답을 한 번은 통째로 본다

sudo kubectl -n keycloak-lab exec a7-probe -- sh -c \
  'curl -s -X POST "http://$K0:8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=password -d client_id=admin-cli \
     -d username=admin -d "password=$PW"'

형태

{"access_token":"eyJhbGciOi...","expires_in":60,"refresh_expires_in":1800,
 "refresh_token":"eyJhbGciOi...","token_type":"Bearer","scope":"profile email"}

어디를 봐야 하는가expires_in 이 60 이다. access token 은 60초짜리고 그동안은 서버에 안 물어본다. 그래서 이 실험의 탐침은 access token 이 아니라 refresh 다 — refresh 는 노드가 세션 저장소를 실제로 뒤져야 답할 수 있다.

하기 — 토큰을 파드 안 파일에 담고, 반대 노드에서 갱신한다

sudo kubectl -n keycloak-lab exec a7-probe -- sh -c \
  'curl -s -X POST "http://$K0:8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=password -d client_id=admin-cli \
     -d username=admin -d "password=$PW" > /tmp/tok
   sed -n "s/.*\"refresh_token\":\"\([^\"]*\)\".*/\1/p" /tmp/tok > /tmp/rt
   echo "rt $(wc -c < /tmp/rt) bytes"'

형태

rt 1188 bytes

★ 길이가 1 bytes 면 빈 문자열에 개행만 들어간 것이다. 파싱이 실패했거나 로그인이 실패한 것이다. cat /tmp/tok 으로 본문을 본다. 이걸 놓치고 진행하면 빈 토큰을 보내고 그 응답을 「세션이 죽었다」로 읽게 된다.

확인 — 반대 노드에서 refresh

sudo kubectl -n keycloak-lab exec a7-probe -- sh -c \
  'curl -s -o /dev/null -w "%{http_code}\n" -X POST \
     "http://$K1:8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=refresh_token -d client_id=admin-cli \
     -d "refresh_token=$(cat /tmp/rt)"'

실측02-a0-rerun.txt

  keycloak-0 로그인 → keycloak-1 에서 refresh  HTTP 200

이 결과가 의미하는 것 — 지금은 교차 노드가 된다. 이 200 이 기준선이다.

refresh token 은 회전한다. 갱신할 때마다 새 것이 나오므로 이어서 또 쓰려면 /tmp/rt 를 다시 채워야 한다. 이 가이드는 각 시험마다 새로 로그인해서 그 문제를 피한다.

1-5. 이 버전에서 정말 끌 수 있나

확인

sudo kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kc.sh build --help-all \
  | tr ',' '\n' | grep -i persistent

실측01-switch-to-volatile.txt 의 전환이 성립한 근거

  persistent-user-sessions[:v1]      ← 목록에 있다

어디를 봐야 하는가 — 이름이 목록에 있는 것.

이 결과가 의미하는 것 — 이 버전(quay.io/keycloak/keycloak:26.7.0)에서는 아직 끌 수 있다. 목록에 없으면 그 버전에서는 이 실험을 할 수 없다 — 기능이 제거되어 기본 동작으로 고정된 것이고, 그 자체가 답이다.

--help-all 은 출력이 길다. tr ',' '\n' 은 한 줄에 쉼표로 이어 붙은 기능 목록을 줄로 쪼개려는 것이다. 처음 한 번은 grep 없이 쳐서 어떤 기능들이 있는지 통째로 본다.


2. 주입 — volatile 로 전환한다

여기부터 상태가 바뀐다. 되돌리는 명령을 먼저 읽어 둔다.

되돌리기 — 5-3 과 같은 명령이다

sudo kubectl -n keycloak-lab patch statefulset keycloak --type=json \
  -p '[{"op":"replace","path":"/spec/template/spec/containers/0/args","value":["start"]}]'
sudo kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s

2-1. 먼저 세션을 비운다 — 비교 기준을 맞추기 위해

하기

sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
  -c "delete from offline_user_session"

실측01-switch-to-volatile.txt

DELETE 151

되돌리기없다. 지운 세션은 돌아오지 않는다.

왜 지우나 — 전환 후 「DB 가 0건」을 확인할 텐데, 테이블에 옛 행이 남아 있으면 0건이 될 수 없다. volatile 은 새로 쓰지 않을 뿐 옛 행을 지우지도 않는다. 이 한 줄을 빼먹으면 3-2 에서 「전환이 안 됐다」고 잘못 읽는다.

이건 실험대라서 하는 일이다. 운영에서 이 명령은 전원 로그아웃이다. 어차피 전환 자체가 세션을 날리므로 순서만 앞당기는 것이지만, 명령 자체가 파괴적이라는 것은 알고 친다.

2-2. args 를 바꾼다

두 가지 방법이 있다. 매니페스트를 고치는 쪽을 권한다 — 무엇이 바뀌었는지 파일에 남는다.

하기 ① — 매니페스트 편집

vim deploy/lab/k8s/keycloak-cluster.yaml
# 149번째 줄 근처
args: ["start", "--features-disabled=persistent-user-sessions"]
sudo kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml

하기 ② — 파일을 안 건드리고 싶으면 patch

sudo kubectl -n keycloak-lab patch statefulset keycloak --type=json \
  -p '[{"op":"replace","path":"/spec/template/spec/containers/0/args",
        "value":["start","--features-disabled=persistent-user-sessions"]}]'

하기 — 롤아웃이 끝날 때까지 기다린다

date '+%H:%M:%S 전환'
sudo kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s

실측01-switch-to-volatile.txt

statefulset.apps/keycloak configured
Waiting for 1 pods to be ready...
partitioned roll out complete: 2 new pods have been updated...

어디를 봐야 하는가configured 가 나와야 한다. unchangedargs 가 안 바뀐 것이다.

이 결과가 의미하는 것--features-disabled 는 빌드 옵션이다. 기동 시 재빌드가 일어나 평소보다 오래 걸린다. --timeout=500s 를 주는 이유가 이것이고, --timeout=60s 로 주면 멀쩡한 롤아웃을 실패로 읽는다.

시각을 반드시 적어 둔다. 뒤에서 지표가 「언제부터 변했나」를 볼 때 이 시각이 없으면 인과를 못 붙인다.


3. 주입이 실제로 걸렸는지 확인한다

결과를 해석하기 전에, 주입이 의도한 것만 건드렸는지 먼저 본다.

3-1. args 가 정말 바뀌었나

확인

sudo kubectl -n keycloak-lab get statefulset keycloak \
  -o jsonpath='{.spec.template.spec.containers[0].args}' ; echo
sudo kubectl -n keycloak-lab get pods -o wide | grep keycloak

실측01-switch-to-volatile.txt

["start","--features-disabled=persistent-user-sessions"]

어디를 봐야 하는가 — 두 가지다.

  • args 문자열이 바뀐 것
  • 파드가 실제로 새것인 것AGE 가 방금이고 RESTARTS0

StatefulSet 의 spec 은 바뀌었는데 파드가 옛 것이면 선언만 바뀌고 프로세스는 그대로다. 그 상태에서 재면 persistent 를 재면서 volatile 이라고 적게 된다.

IP 가 바뀌었으므로 다시 잡는다. 여기서 안 잡으면 4절이 통째로 헛돈다.

K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
K1=$(sudo kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
echo "$K0 $K1"

탐침 파드의 환경변수도 낡았다. 지우고 새 IP 로 다시 띄운다.

sudo kubectl -n keycloak-lab delete pod a7-probe --ignore-not-found
sudo kubectl -n keycloak-lab run a7-probe --image=curlimages/curl:8.11.1 \
  --restart=Never \
  --env="K0=$K0" --env="K1=$K1" \
  --env="PW=$(sudo kubectl -n keycloak-lab get secret keycloak-lab-secrets \
              -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)" \
  --command -- sleep 7200
sudo kubectl -n keycloak-lab wait --for=condition=Ready pod/a7-probe --timeout=120s

3-2. ★ 진짜 판정 — 로그인해도 DB 에 행이 안 생긴다

args 문자열만으로는 부족하다. 동작이 바뀐 것을 봐야 한다.

하기keycloak-0 에만 로그인 5회

sudo kubectl -n keycloak-lab exec a7-probe -- sh -c \
  'for i in 1 2 3 4 5; do
     curl -s -o /dev/null -w "%{http_code} " -X POST \
       "http://$K0:8080/realms/master/protocol/openid-connect/token" \
       -d grant_type=password -d client_id=admin-cli \
       -d username=admin -d "password=$PW"
   done; echo'

형태

200 200 200 200 200

확인 — DB 를 본다

sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
  -c "select offline_flag, count(*) from offline_user_session group by offline_flag"

실측02-a0-rerun.txt

=== DB 에는 들어갔는가 (persistent 였을 때는 5건이 들어갔다) ===
 offline_flag | count
--------------+-------
(0 rows)

어디를 봐야 하는가(0 rows). 이것이 전환의 유일한 확실한 증거다.

이 결과가 의미하는 것 — 로그인 5회가 성공했는데 DB 에 아무것도 안 남았다. 세션이 메모리에만 있다.

3-3. ★ 캐시 엔트리 수로는 두 모드를 구별할 수 없다

여기가 이 실험에서 가장 헷갈리는 자리다.

확인

sudo kubectl -n observability exec deploy/prometheus -- \
  wget -qO- 'localhost:9090/api/v1/query?query=vendor_statistics_approximate_entries_unique'

한 줄짜리 JSON 이 통째로 나온다. 처음 한 번은 그대로 본다. 어떤 라벨이 붙어 있는지 알아야 다음부터 무엇으로 걸러야 할지 안다.

형태

{"status":"success","data":{"resultType":"vector","result":[
{"metric":{"__name__":"vendor_statistics_approximate_entries_unique","cache":"sessions","node":"kc-lab-2","pod":"keycloak-0"},"value":[1757046000.1,"5"]},
{"metric":{"__name__":"vendor_statistics_approximate_entries_unique","cache":"sessions","node":"kc-lab-1","pod":"keycloak-1"},"value":[1757046000.1,"0"]}]}}

라벨을 보고 나면 읽기 좋게 자른다. 미검증

sudo kubectl -n observability exec deploy/prometheus -- \
  wget -qO- 'localhost:9090/api/v1/query?query=vendor_statistics_approximate_entries_unique' \
  | tr ',' '\n' | grep -E '"cache":|"pod":|^"[0-9]'

실측02-a0-rerun.txt

  keycloak-0  sessions 캐시 5.0 건
  keycloak-1  sessions 캐시 0.0 건

어디를 봐야 하는가5 / 0.

이 결과가 의미하는 것persistent 였을 때와 똑같은 숫자다. approximate_entries_unique그 노드가 소유한 엔트리만 센다. 백업본을 들고 있어도 0 으로 보인다.

persistent volatile
로그인 5회 후 캐시 5 / 0 5 / 0
로그인 5회 후 DB 5건 0건

이 지표만 보고 「전환이 안 됐다」고 판단하면 틀린다. 두 모드를 가르는 것은 DB 행이 있느냐이고, 그다음은 7800 을 끊어 보는 것이다. 그게 4-3 이다.


4. 효과를 관찰한다 — 같은 실험 네 개를 다시 돌린다

4-1. A-0 재실행 — DB 는 비었는데 교차 노드가 된다

확인 — 1-4 와 완전히 같은 명령이다

sudo kubectl -n keycloak-lab exec a7-probe -- sh -c \
  'curl -s -X POST "http://$K0:8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=password -d client_id=admin-cli \
     -d username=admin -d "password=$PW" > /tmp/tok
   sed -n "s/.*\"refresh_token\":\"\([^\"]*\)\".*/\1/p" /tmp/tok > /tmp/rt
   curl -s -o /dev/null -w "%{http_code}\n" -X POST \
     "http://$K1:8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=refresh_token -d client_id=admin-cli \
     -d "refresh_token=$(cat /tmp/rt)"'

실측02-a0-rerun.txt

=== 교차 노드 세션은 되는가 ===
  keycloak-0 로그인 → keycloak-1 에서 refresh  HTTP 200

이 결과가 의미하는 것겉보기 결과가 persistent 때와 같다. 그런데 DB 는 0건이다(3-2). 즉 경로가 완전히 달라졌다.

   persistent :  keycloak-1 이 PostgreSQL 을 읽어서 답했다
   volatile   :  keycloak-1 이 7800 을 통해 keycloak-0 에게 물어서 답했다

같은 200 인데 다른 이유다. 겉보기 결과만으로는 구별이 안 된다는 것이 이 절의 요지고, 구별하려면 그 경로를 끊어 봐야 한다.

4-2. A-8 재실행 — 롤링 재시작이 곧 로그아웃

하기 — 재시작 전에 로그인해서 토큰을 파드 안에 보관한다

sudo kubectl -n keycloak-lab exec a7-probe -- sh -c \
  'curl -s -X POST "http://$K0:8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=password -d client_id=admin-cli \
     -d username=admin -d "password=$PW" > /tmp/tok
   sed -n "s/.*\"refresh_token\":\"\([^\"]*\)\".*/\1/p" /tmp/tok > /tmp/rt
   sed -n "s/.*\"access_token\":\"\([^\"]*\)\".*/\1/p" /tmp/tok \
     | cut -d. -f2 | base64 -d 2>/dev/null; echo'

형태 — access token 의 가운데 토막이 클레임이다

{"exp":1757046060,"iat":1757046000,"jti":"...","typ":"Bearer","azp":"admin-cli",
 "sid":"aVwYnzKZFFvMqD3bpSeiILuM",...}

실측03-a8-rerun-restart.txt

=== [A-8 재실행] 재시작 전 로그인 ===
  sid = aVwYnzKZFFvMqD3bpSeiILuM

sid 를 적어 둔다.

base64 패딩 때문에 끝이 깨져 보일 수 있다(2>/dev/null 이 그 불평을 지운다). sid 는 앞쪽에 있어서 대개 보인다.

★ 탐침 파드가 StatefulSet 밖에 있어야 한다. 토큰이 재시작을 넘어 살아 있어야 이 시험이 성립한다. a7-probe--restart=Never 로 띄운 단독 파드라 Keycloak 롤아웃과 무관하다.

하기 — 롤링 재시작

date '+%H:%M:%S 재시작'
sudo kubectl -n keycloak-lab rollout restart statefulset/keycloak
sudo kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s

실측 — 같은 파일

statefulset.apps/keycloak restarted
partitioned roll out complete: 2 new pods have been updated...

되돌리기없다. 롤링 재시작은 정상 작업이고 되돌릴 것이 없다. 다만 파드 IP 가 또 바뀌므로 다시 잡는다.

K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
K1=$(sudo kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
echo "$K0 $K1"

★ 탐침 파드의 K0 환경변수는 낡았다. 하지만 지금은 파드를 다시 띄우면 안 된다 — /tmp/rt 가 같이 사라진다. 대신 새 IP 를 명령줄에 직접 넘긴다.

확인 — 재시작 전 토큰이 아직 통하는가

sudo kubectl -n keycloak-lab exec a7-probe -- sh -c \
  'curl -s -w "\n%{http_code}\n" -X POST \
     "http://'"$K0"':8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=refresh_token -d client_id=admin-cli \
     -d "refresh_token=$(cat /tmp/rt)"'

실측03-a8-rerun-restart.txt

=== ★ 재시작 전 토큰이 아직 통하는가 (persistent 였을 때는 200) ===
  keycloak-0 에서 refresh  HTTP 400
  --- 오류 본문 ---
{"error":"invalid_grant","error_description":"Session not active"}

어디를 봐야 하는가400본문의 Session not active.

이 결과가 의미하는 것A-8 의 결과가 정확히 뒤집혔다. 같은 명령, 같은 순서, 반대 답이다.

persistent (A-8) volatile (지금)
재시작 전 토큰으로 refresh 200 400 Session not active
배포 자유롭다 모든 사용자가 다시 로그인
파드 재시작(OOM·노드 교체) 무해 그 노드가 처리하던 세션 소멸

본문을 반드시 본다. 400 만 보면 「토큰이 이상한가」로 읽히지만, Session not active서버가 그 세션을 모른다는 뜻이다. 토큰은 멀쩡하다.

확인 — 캐시는 어떻게 되었나

sudo kubectl -n observability exec deploy/prometheus -- \
  wget -qO- 'localhost:9090/api/v1/query?query=vendor_statistics_approximate_entries_unique' \
  | tr ',' '\n' | grep -E '"cache":|"pod":|^"[0-9]'

실측 — 같은 파일

=== 캐시 상태 ===
  keycloak-1  sessions 캐시 1.0 건

이 결과가 의미하는 것 — 재시작으로 캐시가 비었고, 방금 실패한 요청이 새 세션을 하나 만든 것이 1건이다. 옛 세션 5건은 어디에도 없다.

24 이전 버전을 쓰는 곳에서 「배포하면 로그아웃된다」가 당연하게 여겨졌던 이유가 이것이다. A-8 이 「이것이 persistent 를 켜는 진짜 이유」라고 쓴 문장이 여기서 증명된다.

4-3. A-1 재실행 — 이번에는 세션 공유가 깨진다

이 절이 이 실험의 핵심이다. A-1 과 같은 주입, 같은 관측, 정반대 결과.

왜 NetworkPolicy 가 아니라 iptables 인가

A-1 에서 배운 것이다. NetworkPolicy 는 conntrack 의 ESTABLISHED 를 못 뚫는다 — 이미 붙어 있는 7800 연결은 계속 산다. A-5 가 그 벽을 넘는 방법을 확립했다.

   패킷 도착
      ├─▶ raw PREROUTING        ← conntrack 보다 먼저. 여기서 끊는다
      ├─▶ conntrack: ESTABLISHED 면 통과
      └─▶ NetworkPolicy 평가    ← 여기까지 오지 않는다

raw 테이블은 CNI 가 안 쓰는 테이블이라 규칙이 밀려나지도 않는다.

되돌리기 — 먼저 읽어 둔다. 두 노드 모두

sudo iptables -t raw -F PREROUTING
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'

하기 — 각 노드에 그 노드에 있는 파드로 들어가는 7800·57800 을 버린다

K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
K1=$(sudo kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')

sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K1 --dport 7800  -j DROP
sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K1 --dport 57800 -j DROP
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 7800  -j DROP"
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 57800 -j DROP"
date '+%H:%M:%S 차단'

어디에 무엇을 넣는지 헷갈리지 않는다.

노드 그 노드에 있는 파드 규칙의 -d
kc-lab-1 keycloak-1 $K1
kc-lab-2 keycloak-0 $K0

57800 도 같이 막는다. FD_SOCK2(장애 감지 채널)는 bind_port + 50000 을 쓴다. 7800 만 막으면 장애 감지가 살아 있어 분단이 어중간해진다.

확인 — 규칙이 걸렸고 패킷을 실제로 세고 있나

sudo iptables -t raw -L PREROUTING -n -v
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n -v'

형태

Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target  prot opt in  out  source     destination
   19  1140 DROP    tcp  --  *   *    0.0.0.0/0  10.42.0.46  tcp dpt:7800
    0     0 DROP    tcp  --  *   *    0.0.0.0/0  10.42.0.46  tcp dpt:57800

어디를 봐야 하는가pkts 카운터. 규칙이 목록에 있는데 pkts 가 0 이면 패킷이 그 경로로 안 오는 것이고, 분단은 안 만들어졌다. A-5 가 이 함정에 두 번 빠졌다.

확인 — 분단이 성립했나. 25초 간격으로 몇 번 친다

sudo kubectl -n observability exec deploy/prometheus -- \
  wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
  | tr ',' '\n' | grep -E '"pod":|^"[0-9]'

실측04-a1-rerun-partition.txt

  차단 적용 (A-5 에서 확인한 raw 테이블 방식, 양방향)
  분단이 성립할 때까지 대기...
    +25초  cluster_size(k0 k1) = [2.0 2.0 ]
    +50초  cluster_size(k0 k1) = [1.0 ]
    +75초  cluster_size(k0 k1) = [1.0 ]
    +100초  cluster_size(k0 k1) = []
    +125초  cluster_size(k0 k1) = [1.0 ]

어디를 봐야 하는가2.0 2.01.0 으로 떨어지는 것. 50초쯤 걸린다.

[] 와 값이 하나뿐인 줄은 측정 실패다. 원래 실행은 20~25초마다 임시 파드를 띄워 지표를 긁는 스크립트를 썼는데, 파드 생성이 느리고 경합이 있어 빈 응답이 섞였다. A-1 가이드가 지적한 그 문제가 여기서도 그대로 보인다. 당신은 손으로 치므로 빈 값이 나오면 그 자리에서 보이고 다시 치면 된다. 빈 값을 「0으로 떨어졌다」로 읽지 않는다.

확인 — split brain 을 DB 한 줄로

sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
  -c "select name, ip, coord from jgroups_ping order by name"

형태

       name       |       ip        | coord
------------------+-----------------+-------
 keycloak-0-30843 | 10.42.1.99:7800 | t
 keycloak-1-48749 | 10.42.0.46:7800 | t

coord = t 가 둘이면 분단이다. 정상일 때는 하나다.

본 시험 — 대조군과 시험군을 같이 잰다

★ 대조군을 반드시 같이 잰다. 차단이 모든 것을 망가뜨린 게 아니라 교차 노드만 끊었다는 것을 보여야 한다.

하기 — 같은 노드(대조군)

sudo kubectl -n keycloak-lab exec a7-probe -- sh -c \
  'curl -s -X POST "http://'"$K0"':8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=password -d client_id=admin-cli \
     -d username=admin -d "password=$PW" > /tmp/tok
   sed -n "s/.*\"refresh_token\":\"\([^\"]*\)\".*/\1/p" /tmp/tok > /tmp/rt
   curl -s -o /dev/null -w "same-node %{http_code}\n" -X POST \
     "http://'"$K0"':8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=refresh_token -d client_id=admin-cli \
     -d "refresh_token=$(cat /tmp/rt)"'

하기 — 교차 노드(시험군). 새로 로그인해서 새 토큰으로 한다

sudo kubectl -n keycloak-lab exec a7-probe -- sh -c \
  'curl -s -X POST "http://'"$K0"':8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=password -d client_id=admin-cli \
     -d username=admin -d "password=$PW" > /tmp/tok
   sed -n "s/.*\"refresh_token\":\"\([^\"]*\)\".*/\1/p" /tmp/tok > /tmp/rt
   curl -s -w "\ncross-node %{http_code}\n" -X POST \
     "http://'"$K1"':8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=refresh_token -d client_id=admin-cli \
     -d "refresh_token=$(cat /tmp/rt)"'

실측04-a1-rerun-partition.txt

=== ★ 분단 상태에서 교차 노드 세션 (persistent 였을 때는 200) ===
  keycloak-0 로그인 → keycloak-0 에서 refresh  HTTP 200  ← 대조군
  keycloak-0 로그인 → keycloak-1 에서 refresh  HTTP 400  ← 시험군
  --- 시험군 오류 본문 ---
{"error":"invalid_grant","error_description":"Session not active"}

이 결과가 의미하는 것 — 이 한 쌍이 A층 전체의 근거다.

   persistent :  세션 ── PostgreSQL ──▶ 양쪽이 본다      7800 무관
   volatile   :  세션 ── 클러스터(7800) ─▶ 상대에게 간다   7800 필수

A-1 이 통념과 어긋난 이유가 확정됐다. 통념은 24 이전에서 맞다. 틀린 것은 자료가 아니라 버전을 확인하지 않고 적용하는 것이다.

하기 — 차단을 푼다. 다음 절로 넘어가기 전에 반드시 푼다

sudo iptables -t raw -F PREROUTING
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
sudo iptables -t raw -L PREROUTING -n
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n'

확인 — 클러스터가 다시 붙었나. 1~2분 기다린다

sudo kubectl -n observability exec deploy/prometheus -- \
  wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
  | tr ',' '\n' | grep -E '"pod":|^"[0-9]'

양쪽이 2 로 돌아와야 4-4 로 넘어간다. 분단이 남아 있으면 4-4 의 결과가 DB 때문인지 분단 때문인지 구별되지 않는다.

4-4. A-2 재실행 — 새 로그인은 되는데 refresh 가 안 된다

되돌리기 — 먼저 읽어 둔다

sudo kubectl -n keycloak-lab scale deployment/postgres --replicas=1
sudo kubectl -n keycloak-lab rollout status deployment/postgres --timeout=180s

하기 — DB 를 내리기 전에 로그인해서 토큰을 확보한다

sudo kubectl -n keycloak-lab exec a7-probe -- sh -c \
  'curl -s -X POST "http://'"$K0"':8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=password -d client_id=admin-cli \
     -d username=admin -d "password=$PW" > /tmp/tok
   sed -n "s/.*\"refresh_token\":\"\([^\"]*\)\".*/\1/p" /tmp/tok > /tmp/rt
   echo "rt $(wc -c < /tmp/rt) bytes"'

하기 — PostgreSQL 을 0대로

date '+%H:%M:%S 정지'
sudo kubectl -n keycloak-lab scale deployment/postgres --replicas=0
sudo kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s

실측05-a2-rerun-db-loss.txt

deployment.apps/postgres scaled
  postgres 정지

scale --replicas=0 인 이유delete pod 은 Deployment 가 곧바로 새로 만든다. DB 가 없는 구간을 원하는 만큼 유지할 수 있어야 두 경로를 다 잰다.

확인 — ① 캐시를 가진 노드에서 refresh

sudo kubectl -n keycloak-lab exec a7-probe -- sh -c \
  'curl -s -o /dev/null -w "%{http_code}\n" -X POST \
     "http://'"$K0"':8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=refresh_token -d client_id=admin-cli \
     -d "refresh_token=$(cat /tmp/rt)"'

확인 — ② 새 로그인

sudo kubectl -n keycloak-lab exec a7-probe -- sh -c \
  'curl -s -o /dev/null -w "%{http_code}\n" -X POST \
     "http://'"$K0"':8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=password -d client_id=admin-cli \
     -d username=admin -d "password=$PW"'

실측05-a2-rerun-db-loss.txt

  ① 캐시를 가진 노드에서 refresh  HTTP 500
  ② 새 로그인                     HTTP 200

어디를 봐야 하는가순서가 거꾸로다. persistent 에서는 새 로그인이 500 이었다. 세션을 DB 에 써야 했기 때문이다. 그 쓰기가 없어지니 로그인이 통과한다.

   로그인에 필요한 것
     ├─ realm 설정   → Infinispan `realms` 캐시에 있다
     ├─ 사용자 자격  → `users` 캐시에 있다
     └─ 세션 저장    → volatile 이므로 메모리
   → DB 없이 완결된다

★ 이 두 숫자를 그대로 표로 옮기면 안 된다

이 결과는 조건부다. 후속 실험 A-7a 가 확정한 것:

캐시 상태 로그인 refresh
완전 냉시동 (재시작 직후) 400 400
CLIENT 만 더움 ← 위에서 잰 것 200 500
완전히 더움 200 200

같은 설정에서 캐시 온도만으로 셋으로 갈린다. 위에서 잰 200 / 500 은 그중 한 상태다 — 마침 롤아웃 뒤 로그인을 몇 번 했고 refresh 는 안 한 상태였기 때문에 그 값이 나왔다.

그리고 A-7 이 남긴 「refresh 가 500 인 이유는 REVOKED_TOKEN 조회일 것」이라는 가설은 틀렸다. 실제 원인은 CLIENT_SCOPE_CLIENTDEFAULT_SCOPE='f' 로 조회하는 한 문장이고, 그것은 문장 로깅을 켜야 보인다.

한 번 재고 표로 적으면 안 되는 종류의 측정이다. 상태가 결과를 바꾸는데 그 상태가 안 보인다. A-1 에서 conntrack 이 「주입했는데 안 걸렸다」를 만든 것과 같은 계열의 함정이다. 셋 다 재현하는 절차는 A-7a 가이드 에 있다.

하기 — DB 를 되살린다

sudo kubectl -n keycloak-lab scale deployment/postgres --replicas=1
sudo kubectl -n keycloak-lab rollout status deployment/postgres --timeout=180s

실측05-a2-rerun-db-loss.txt

deployment.apps/postgres scaled
deployment "postgres" successfully rolled out

volatile 이 「DB 없이 돌아간다」는 뜻은 아니다. realm·사용자·클라이언트· 취소 토큰은 여전히 DB 에 있다. 세션만 메모리로 옮긴 것이다.


5. 복구

5-1. iptables 가 남아 있지 않은지 먼저 본다

확인

sudo iptables -t raw -L PREROUTING -n
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n'

규칙이 남아 있으면 지운다.

sudo iptables -t raw -F PREROUTING
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'

5-2. PostgreSQL 이 떠 있는지 본다

확인

sudo kubectl -n keycloak-lab get pods -l app=postgres

Running 이 아니면 scale deployment/postgres --replicas=1.

5-3. args 를 되돌린다

하기

sudo kubectl -n keycloak-lab patch statefulset keycloak --type=json \
  -p '[{"op":"replace","path":"/spec/template/spec/containers/0/args","value":["start"]}]'
sudo kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s

매니페스트를 고쳤다면 파일도 같이 되돌린다. 안 그러면 다음에 apply 할 때 volatile 로 다시 간다.

git diff deploy/lab/k8s/keycloak-cluster.yaml
git checkout -- deploy/lab/k8s/keycloak-cluster.yaml

실측06-restore-persistent.txt

=== persistent 모드로 원복 ===
statefulset.apps/keycloak configured
partitioned roll out complete: 2 new pods have been updated...

5-4. 정말 돌아왔는지 — 로그인 후 DB 에 행이 생기는가

args 문자열만 보고 끝내지 않는다. 3-2 와 같은 이유로, 동작을 봐야 한다.

하기 — 새 IP 로 탐침을 다시 띄우고 로그인 한 번

sudo kubectl -n keycloak-lab delete pod a7-probe --ignore-not-found
K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
sudo kubectl -n keycloak-lab run a7-probe --image=curlimages/curl:8.11.1 \
  --restart=Never --env="K0=$K0" \
  --env="PW=$(sudo kubectl -n keycloak-lab get secret keycloak-lab-secrets \
              -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)" \
  --command -- sleep 600
sudo kubectl -n keycloak-lab wait --for=condition=Ready pod/a7-probe --timeout=120s
sudo kubectl -n keycloak-lab exec a7-probe -- sh -c \
  'curl -s -o /dev/null -w "%{http_code}\n" -X POST \
     "http://$K0:8080/realms/master/protocol/openid-connect/token" \
     -d grant_type=password -d client_id=admin-cli \
     -d username=admin -d "password=$PW"'

확인

sudo kubectl -n keycloak-lab get statefulset keycloak \
  -o jsonpath='{.spec.template.spec.containers[0].args}' ; echo
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
  -tAc "select count(*) from offline_user_session where offline_flag='0'"

실측06-restore-persistent.txt

["start"]
로그인
  DB 온라인 세션: 1 건  (1 이면 persistent 복귀)
keycloak-0                  1/1   Running   0     67s
keycloak-1                  1/1   Running   0     89s
postgres-7b474b88c8-t6rrf   1/1   Running   0     2m8s
  외부 진입점 HTTP 200

어디를 봐야 하는가1 건. 2-1 에서 테이블을 비웠으므로 여기서 세는 값은 방금 만든 세션 하나뿐이다. 0 이면 아직 volatile 이다.

5-5. 원상복구 확인표

항목 명령 돌아왔을 때
args sudo kubectl -n keycloak-lab get statefulset keycloak -o jsonpath='{.spec.template.spec.containers[0].args}' ["start"]
매니페스트 git diff deploy/lab/k8s/keycloak-cluster.yaml 출력 없음
파드 sudo kubectl -n keycloak-lab get pods -o wide keycloak 둘 다 1/1 Running
DB sudo kubectl -n keycloak-lab get pods -l app=postgres 1/1 Running
동작 위 5-4 로그인 후 세션 행이 생긴다
iptables sudo iptables -t raw -L PREROUTING -n (두 노드) 규칙 없음
클러스터 vendor_cluster_size 양쪽 2
탐침 파드 sudo kubectl -n keycloak-lab get pod a7-probe NotFound
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master 200
sudo kubectl -n keycloak-lab delete pod a7-probe --ignore-not-found

이 실험이 재지 않은 것 — volatile 상태에서 노드를 추가했을 때 복제 트래픽이 어떻게 늘어나는지는 재지 않았다. 파드가 둘뿐이라 N² 를 볼 수 없다.


막히면

전부 이 실험대가 실제로 겪은 증상이다. 지어낸 것은 없다.

증상 원인 확인
rollout status 가 타임아웃 빌드 옵션이라 재빌드가 일어난다. 평소보다 오래 걸린다 --timeout=500s 로 다시. logs keycloak-0 에 빌드 진행이 보인다
applyunchanged args 를 안 고쳤거나 다른 파일을 고쳤다 get statefulset ... -o jsonpath='{...args}' 로 실제 값
전환했는데 DB 에 행이 그대로 2-1 의 delete 를 건너뛰었다. 옛 행은 안 지워진다 delete from offline_user_session 후 다시 로그인
캐시가 5 / 0 이라 전환이 안 된 것 같다 두 모드가 같은 값을 낸다 판정은 DB 행 수로 한다 — 3-3
차단했는데 cluster_size 가 계속 2 규칙이 안 걸렸거나 pkts 가 0 iptables -t raw -L PREROUTING -n -v 의 카운터 — 4-3
cluster_size 결과가 [] 측정 실패다. 원래 실행의 스크립트가 빈 값을 뱉었다 손으로 다시 친다. 빈 값은 판정에서 뺀다
교차 노드가 계속 200 차단이 한쪽만 걸렸다 = 단방향 두 노드 카운터를 둘 다 본다
재시작 뒤 아무 데도 안 닿는다 파드 IP 가 바뀌었다 get pod -o jsonpath='{.status.podIP}' 다시
refresh 가 400 인데 이유를 모르겠다 본문을 안 봤다 -o /dev/null 을 빼고 본문을 본다. Session not active 인지
A-2 재실행이 200 / 200 이 나온다 캐시가 이미 더워졌다. 틀린 게 아니다 조건부다 — 4-4 의 표, A-7a
로그인이 400 unauthorized_client 완전 냉시동이다. 클라이언트 조회조차 캐시에 없다 이것도 조건부 — A-7a
kubectl exec keycloak-0 -- curlexit 127 Keycloak 이미지에 curl 도 wget 도 없다 탐침 파드를 쓴다
다음 실험 결과가 이상하다 원복을 안 했다 5-5 확인표를 전부 통과시킨다

다음

실험 A-7 이 남긴 질문
A-7a volatile 원인 확정 4-4 의 500 은 왜인가. 가설(REVOKED_TOKEN)은 틀렸고, 표 자체가 조건부다
A-1 7800 차단 같은 주입, 정반대 결과. 이 둘을 나란히 놓는 것이 A층의 근거다
A-8 롤링 재시작 「배포하면 로그아웃」이 왜 옛 상식이었는지
전부 버전 확인이 1순위다. 인터넷 자료가 틀린 게 아니라 버전이 다른 것이다