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

1073 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# A-7 재현 가이드 — 옛 방식으로 바꿔서 A층 결론이 뒤집히는 것을 직접 본다
해설 문서: [`docs/experiment-a7-volatile-comparison.md`](../../experiment-a7-volatile-comparison.md) ·
증거 원문: [`docs/evidence/a7-volatile-comparison/`](../../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`](../05-keycloak/) · [`06-observability`](../06-observability/) 가 끝나 있다.
- **[A-1](a1-jgroups-transport-block.md) · [A-2](a2-database-loss.md) ·
[A-8](a8-rolling-restart.md) 을 먼저 해 두면 좋다.** 이 실험은 그 셋의
**대조군**이고, 기준선을 몸으로 알고 있어야 「뒤집혔다」가 보인다.
- 명령은 **`kc-lab-1` 에서** 친다. `kubectl``sudo` 로 쓴다.
- `kc-lab-2` 에는 `ssh kc-lab-2` 로 붙는다. 4-3 의 iptables 는 **두 노드에 각각**
넣는다.
- 터미널 **두 개**를 열어 두면 편하다. 하나는 관찰용, 하나는 대기용.
## 주의 — 이건 클러스터의 동작 모드를 바꾸는 실험이다
`persistent-user-sessions` 를 끈다. **전환하는 순간 기존 세션이 전부 사라지고**,
되돌릴 때 또 한 번 사라진다. 빌드 옵션이라 기동 시 재빌드가 일어나 롤아웃이
평소보다 오래 걸린다(`--timeout=500s` 를 주는 이유다).
**실험대에서만 한다.** 전 구간 약 40~60분이고, 되돌리는 방법은 매 단계에 적어
두었다. 중간에 그만두려면 [5. 복구](#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. 노드와 파드
**확인**
```bash
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`, `RESTARTS``0`
- **`NODE` 가 서로 다르다** — 같은 노드면 4-3 의 노드 간 차단이 성립하지 않는다
- **파드 번호와 노드 번호가 어긋난다.** `keycloak-0``kc-lab-2` 에 있다.
4-3 에서 iptables 를 어느 노드에 넣을지 정할 때 이걸 헷갈리면 규칙은 걸리는데
아무 일도 안 일어난다
IP 는 변수로 잡아 둔다. **파드가 재시작되면 바뀌므로** 그때마다 다시 잡는다.
이 실험은 롤아웃을 세 번 하므로 **세 번 다시 잡는다.**
```bash
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`](../../evidence/a7-volatile-comparison/02-a0-rerun.txt)
```
10.42.1.94 10.42.0.45
```
## 1-2. 지금 args 가 무엇인가 — **이것이 되돌릴 값이다**
**확인**
```bash
sudo kubectl -n keycloak-lab get statefulset keycloak \
-o jsonpath='{.spec.template.spec.containers[0].args}' ; echo
```
**실측** — [`06-restore-persistent.txt`](../../evidence/a7-volatile-comparison/06-restore-persistent.txt)
```
["start"]
```
**어디를 봐야 하는가**`["start"]` 하나뿐이다. 플래그가 없다.
**이 결과가 의미하는 것** — 기능 플래그를 아무것도 주지 않았으므로 **26 의
기본값**으로 돌고 있다. `persistent-user-sessions` 가 켜져 있는 상태다.
**이 문자열을 적어 둔다.** 5-3 에서 이 값 그대로 되돌린다.
## 1-3. DB 에 세션 행이 있다 — persistent 의 증거
**확인**
```bash
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 컨테이너에는 `curl``wget` 도 없다(`exit 127`). 탐침 파드를 띄운다.
**이 파드는 실험 내내 살려 둔다** — 롤링 재시작을 넘어 토큰을 들고 있어야 하기
때문이다.
**하기**
```bash
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
```
**되돌리기**
```bash
sudo kubectl -n keycloak-lab delete pod a7-probe --ignore-not-found
```
> **비밀번호를 화면에 찍지 않는다.** 명령 치환으로 넘기므로 값은 터미널에도 셸
> 히스토리에도 남지 않는다. 존재와 길이만 확인하고 싶으면:
> ```bash
> sudo kubectl -n keycloak-lab get secret keycloak-lab-secrets \
> -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
> ```
> **실측** — `19`
**확인** — 환경변수가 들어갔나
```bash
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` 에서 로그인한다. 응답을 **한 번은 통째로 본다**
```bash
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"'
```
**형태**
```json
{"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 는 노드가 세션 저장소를 실제로 뒤져야 답할 수 있다.
**하기** — 토큰을 파드 안 파일에 담고, 반대 노드에서 갱신한다
```bash
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
```bash
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`](../../evidence/a7-volatile-comparison/02-a0-rerun.txt)
```
keycloak-0 로그인 → keycloak-1 에서 refresh HTTP 200
```
**이 결과가 의미하는 것** — 지금은 교차 노드가 된다. **이 200 이 기준선이다.**
> **refresh token 은 회전한다.** 갱신할 때마다 새 것이 나오므로 이어서 또 쓰려면
> `/tmp/rt` 를 다시 채워야 한다. 이 가이드는 각 시험마다 **새로 로그인**해서
> 그 문제를 피한다.
## 1-5. 이 버전에서 정말 끌 수 있나
**확인**
```bash
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`](../../evidence/a7-volatile-comparison/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 과 같은 명령이다
```bash
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. 먼저 세션을 비운다 — 비교 기준을 맞추기 위해
**하기**
```bash
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "delete from offline_user_session"
```
**실측** — [`01-switch-to-volatile.txt`](../../evidence/a7-volatile-comparison/01-switch-to-volatile.txt)
```
DELETE 151
```
**되돌리기****없다.** 지운 세션은 돌아오지 않는다.
**왜 지우나** — 전환 후 「DB 가 0건」을 확인할 텐데, **테이블에 옛 행이 남아
있으면 0건이 될 수 없다.** volatile 은 새로 쓰지 않을 뿐 옛 행을 지우지도 않는다.
이 한 줄을 빼먹으면 3-2 에서 「전환이 안 됐다」고 잘못 읽는다.
> **이건 실험대라서 하는 일이다.** 운영에서 이 명령은 전원 로그아웃이다.
> 어차피 전환 자체가 세션을 날리므로 순서만 앞당기는 것이지만, **명령 자체가
> 파괴적이라는 것은 알고 친다.**
## 2-2. args 를 바꾼다
두 가지 방법이 있다. **매니페스트를 고치는 쪽을 권한다** — 무엇이 바뀌었는지
파일에 남는다.
**하기 ①** — 매니페스트 편집
```bash
vim deploy/lab/k8s/keycloak-cluster.yaml
```
```yaml
# 149번째 줄 근처
args: ["start", "--features-disabled=persistent-user-sessions"]
```
```bash
sudo kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
```
**하기 ②** — 파일을 안 건드리고 싶으면 patch
```bash
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"]}]'
```
**하기** — 롤아웃이 끝날 때까지 기다린다
```bash
date '+%H:%M:%S 전환'
sudo kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
```
**실측** — [`01-switch-to-volatile.txt`](../../evidence/a7-volatile-comparison/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` 가 나와야 한다. `unchanged` 면 **args 가
안 바뀐 것**이다.
**이 결과가 의미하는 것****`--features-disabled` 는 빌드 옵션이다.** 기동 시
재빌드가 일어나 평소보다 오래 걸린다. `--timeout=500s` 를 주는 이유가 이것이고,
`--timeout=60s` 로 주면 멀쩡한 롤아웃을 실패로 읽는다.
**시각을 반드시 적어 둔다.** 뒤에서 지표가 「언제부터 변했나」를 볼 때 이 시각이
없으면 인과를 못 붙인다.
---
# 3. 주입이 실제로 걸렸는지 확인한다
**결과를 해석하기 전에, 주입이 의도한 것만 건드렸는지 먼저 본다.**
## 3-1. args 가 정말 바뀌었나
**확인**
```bash
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`](../../evidence/a7-volatile-comparison/01-switch-to-volatile.txt)
```
["start","--features-disabled=persistent-user-sessions"]
```
**어디를 봐야 하는가** — 두 가지다.
- args 문자열이 바뀐 것
- **파드가 실제로 새것인 것** — `AGE` 가 방금이고 `RESTARTS``0`
StatefulSet 의 `spec` 은 바뀌었는데 파드가 옛 것이면 **선언만 바뀌고 프로세스는
그대로**다. 그 상태에서 재면 persistent 를 재면서 volatile 이라고 적게 된다.
IP 가 바뀌었으므로 다시 잡는다. **여기서 안 잡으면 4절이 통째로 헛돈다.**
```bash
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 로 다시 띄운다.**
```bash
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회
```bash
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 를 본다
```bash
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`](../../evidence/a7-volatile-comparison/02-a0-rerun.txt)
```
=== DB 에는 들어갔는가 (persistent 였을 때는 5건이 들어갔다) ===
offline_flag | count
--------------+-------
(0 rows)
```
**어디를 봐야 하는가****`(0 rows)`.** 이것이 전환의 유일한 확실한 증거다.
**이 결과가 의미하는 것** — 로그인 5회가 성공했는데 DB 에 아무것도 안 남았다.
세션이 메모리에만 있다.
## 3-3. ★ 캐시 엔트리 수로는 두 모드를 구별할 수 없다
여기가 이 실험에서 가장 헷갈리는 자리다.
**확인**
```bash
sudo kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_statistics_approximate_entries_unique'
```
한 줄짜리 JSON 이 통째로 나온다. **처음 한 번은 그대로 본다.** 어떤 라벨이
붙어 있는지 알아야 다음부터 무엇으로 걸러야 할지 안다.
**형태**
```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"]}]}}
```
라벨을 보고 나면 읽기 좋게 자른다. **미검증**
```bash
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`](../../evidence/a7-volatile-comparison/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 와 **완전히 같은 명령**이다
```bash
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`](../../evidence/a7-volatile-comparison/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 재실행 — 롤링 재시작이 곧 로그아웃
**하기** — 재시작 **전에** 로그인해서 토큰을 파드 안에 보관한다
```bash
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 의 가운데 토막이 클레임이다
```json
{"exp":1757046060,"iat":1757046000,"jti":"...","typ":"Bearer","azp":"admin-cli",
"sid":"aVwYnzKZFFvMqD3bpSeiILuM",...}
```
**실측** — [`03-a8-rerun-restart.txt`](../../evidence/a7-volatile-comparison/03-a8-rerun-restart.txt)
```
=== [A-8 재실행] 재시작 전 로그인 ===
sid = aVwYnzKZFFvMqD3bpSeiILuM
```
`sid` 를 적어 둔다.
> base64 패딩 때문에 끝이 깨져 보일 수 있다(`2>/dev/null` 이 그 불평을 지운다).
> `sid` 는 앞쪽에 있어서 대개 보인다.
**★ 탐침 파드가 StatefulSet 밖에 있어야 한다.** 토큰이 재시작을 넘어 살아
있어야 이 시험이 성립한다. `a7-probe``--restart=Never` 로 띄운 단독 파드라
Keycloak 롤아웃과 무관하다.
**하기** — 롤링 재시작
```bash
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 가 또 바뀌므로 다시 잡는다.
```bash
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 를 명령줄에 직접 넘긴다.
**확인** — 재시작 전 토큰이 아직 통하는가
```bash
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`](../../evidence/a7-volatile-comparison/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` 는 **서버가 그 세션을 모른다**는 뜻이다. 토큰은 멀쩡하다.
**확인** — 캐시는 어떻게 되었나
```bash
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 가 안 쓰는 테이블**이라 규칙이 밀려나지도 않는다.
**되돌리기** — 먼저 읽어 둔다. **두 노드 모두**
```bash
sudo iptables -t raw -F PREROUTING
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
```
**하기** — 각 노드에 **그 노드에 있는 파드로 들어가는** 7800·57800 을 버린다
```bash
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 만 막으면 장애 감지가 살아 있어 분단이 어중간해진다.
**확인** — 규칙이 걸렸고 **패킷을 실제로 세고 있나**
```bash
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초 간격으로 몇 번 친다
```bash
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`](../../evidence/a7-volatile-comparison/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.0``1.0` 으로 떨어지는 것. **50초쯤 걸린다.**
> **★ `[]` 와 값이 하나뿐인 줄은 측정 실패다.** 원래 실행은 20~25초마다 임시
> 파드를 띄워 지표를 긁는 스크립트를 썼는데, 파드 생성이 느리고 경합이 있어
> **빈 응답이 섞였다.** A-1 가이드가 지적한 그 문제가 여기서도 그대로 보인다.
> 당신은 손으로 치므로 빈 값이 나오면 그 자리에서 보이고 다시 치면 된다.
> **빈 값을 「0으로 떨어졌다」로 읽지 않는다.**
**확인** — split brain 을 DB 한 줄로
```bash
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` 가 둘이면 분단이다.** 정상일 때는 하나다.
### 본 시험 — 대조군과 시험군을 같이 잰다
**★ 대조군을 반드시 같이 잰다.** 차단이 **모든 것을** 망가뜨린 게 아니라
**교차 노드만** 끊었다는 것을 보여야 한다.
**하기** — 같은 노드(대조군)
```bash
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)"'
```
**하기** — 교차 노드(시험군). **새로 로그인해서 새 토큰으로 한다**
```bash
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`](../../evidence/a7-volatile-comparison/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 이전에서 맞다. 틀린 것은
자료가 아니라 **버전을 확인하지 않고 적용하는 것**이다.
**하기** — 차단을 푼다. **다음 절로 넘어가기 전에 반드시 푼다**
```bash
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분 기다린다
```bash
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 가 안 된다
**되돌리기** — 먼저 읽어 둔다
```bash
sudo kubectl -n keycloak-lab scale deployment/postgres --replicas=1
sudo kubectl -n keycloak-lab rollout status deployment/postgres --timeout=180s
```
**하기** — DB 를 내리기 **전에** 로그인해서 토큰을 확보한다
```bash
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대로
```bash
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`](../../evidence/a7-volatile-comparison/05-a2-rerun-db-loss.txt)
```
deployment.apps/postgres scaled
postgres 정지
```
**`scale --replicas=0` 인 이유** — `delete pod` 은 Deployment 가 곧바로 새로
만든다. DB 가 없는 구간을 원하는 만큼 유지할 수 있어야 두 경로를 다 잰다.
**확인** — ① 캐시를 가진 노드에서 refresh
```bash
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)"'
```
**확인** — ② 새 로그인
```bash
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`](../../evidence/a7-volatile-comparison/05-a2-rerun-db-loss.txt)
```
① 캐시를 가진 노드에서 refresh HTTP 500
② 새 로그인 HTTP 200
```
**어디를 봐야 하는가****순서가 거꾸로다.** persistent 에서는 새 로그인이
`500` 이었다. 세션을 DB 에 써야 했기 때문이다. 그 쓰기가 없어지니 로그인이
통과한다.
```
로그인에 필요한 것
├─ realm 설정 → Infinispan `realms` 캐시에 있다
├─ 사용자 자격 → `users` 캐시에 있다
└─ 세션 저장 → volatile 이므로 메모리
→ DB 없이 완결된다
```
### ★ 이 두 숫자를 그대로 표로 옮기면 안 된다
**이 결과는 조건부다.** 후속 실험 [A-7a](a7a-volatile-cause.md) 가 확정한 것:
| 캐시 상태 | 로그인 | refresh |
|---|---|---|
| **완전 냉시동** (재시작 직후) | **400** | 400 |
| **CLIENT 만 더움** ← 위에서 잰 것 | 200 | **500** |
| **완전히 더움** | 200 | **200** |
**같은 설정에서 캐시 온도만으로 셋으로 갈린다.** 위에서 잰 `200 / 500` 은 그중
한 상태다 — 마침 롤아웃 뒤 로그인을 몇 번 했고 refresh 는 안 한 상태였기 때문에
그 값이 나왔다.
그리고 A-7 이 남긴 **「refresh 가 500 인 이유는 `REVOKED_TOKEN` 조회일 것」이라는
가설은 틀렸다.** 실제 원인은 `CLIENT_SCOPE_CLIENT``DEFAULT_SCOPE='f'`
조회하는 한 문장이고, 그것은 **문장 로깅을 켜야 보인다.**
> **한 번 재고 표로 적으면 안 되는 종류의 측정이다.** 상태가 결과를 바꾸는데
> 그 상태가 안 보인다. A-1 에서 conntrack 이 「주입했는데 안 걸렸다」를 만든 것과
> 같은 계열의 함정이다. 셋 다 재현하는 절차는 [A-7a 가이드](a7a-volatile-cause.md) 에 있다.
**하기** — DB 를 되살린다
```bash
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`](../../evidence/a7-volatile-comparison/05-a2-rerun-db-loss.txt)
```
deployment.apps/postgres scaled
deployment "postgres" successfully rolled out
```
> **volatile 이 「DB 없이 돌아간다」는 뜻은 아니다.** realm·사용자·클라이언트·
> 취소 토큰은 **여전히 DB 에 있다.** 세션만 메모리로 옮긴 것이다.
---
# 5. 복구
## 5-1. iptables 가 남아 있지 않은지 먼저 본다
**확인**
```bash
sudo iptables -t raw -L PREROUTING -n
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n'
```
규칙이 남아 있으면 지운다.
```bash
sudo iptables -t raw -F PREROUTING
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
```
## 5-2. PostgreSQL 이 떠 있는지 본다
**확인**
```bash
sudo kubectl -n keycloak-lab get pods -l app=postgres
```
`Running` 이 아니면 `scale deployment/postgres --replicas=1`.
## 5-3. args 를 되돌린다
**하기**
```bash
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 로 다시 간다.
```bash
git diff deploy/lab/k8s/keycloak-cluster.yaml
git checkout -- deploy/lab/k8s/keycloak-cluster.yaml
```
**실측** — [`06-restore-persistent.txt`](../../evidence/a7-volatile-comparison/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 로 탐침을 다시 띄우고 로그인 한 번
```bash
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"'
```
**확인**
```bash
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`](../../evidence/a7-volatile-comparison/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` |
```bash
sudo kubectl -n keycloak-lab delete pod a7-probe --ignore-not-found
```
> **이 실험이 재지 않은 것** — volatile 상태에서 노드를 **추가**했을 때 복제
> 트래픽이 어떻게 늘어나는지는 재지 않았다. 파드가 둘뿐이라 N² 를 볼 수 없다.
---
# 막히면
전부 이 실험대가 **실제로 겪은** 증상이다. 지어낸 것은 없다.
| 증상 | 원인 | 확인 |
|---|---|---|
| `rollout status` 가 타임아웃 | **빌드 옵션이라 재빌드가 일어난다.** 평소보다 오래 걸린다 | `--timeout=500s` 로 다시. `logs keycloak-0` 에 빌드 진행이 보인다 |
| `apply``unchanged` | 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](a7a-volatile-cause.md) |
| 로그인이 `400 unauthorized_client` | **완전 냉시동이다.** 클라이언트 조회조차 캐시에 없다 | 이것도 조건부 — [A-7a](a7a-volatile-cause.md) |
| `kubectl exec keycloak-0 -- curl``exit 127` | Keycloak 이미지에 curl 도 wget 도 없다 | 탐침 파드를 쓴다 |
| 다음 실험 결과가 이상하다 | **원복을 안 했다** | 5-5 확인표를 전부 통과시킨다 |
---
# 다음
| 실험 | A-7 이 남긴 질문 |
|---|---|
| [A-7a](a7a-volatile-cause.md) volatile 원인 확정 | **4-4 의 `500` 은 왜인가.** 가설(`REVOKED_TOKEN`)은 틀렸고, 표 자체가 조건부다 |
| [A-1](a1-jgroups-transport-block.md) 7800 차단 | **같은 주입, 정반대 결과.** 이 둘을 나란히 놓는 것이 A층의 근거다 |
| [A-8](a8-rolling-restart.md) 롤링 재시작 | 「배포하면 로그아웃」이 왜 옛 상식이었는지 |
| 전부 | **버전 확인이 1순위다.** 인터넷 자료가 틀린 게 아니라 버전이 다른 것이다 |