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>
1073 lines
45 KiB
Markdown
1073 lines
45 KiB
Markdown
# 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:22–13: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순위다.** 인터넷 자료가 틀린 게 아니라 버전이 다른 것이다 |
|