# 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순위다.** 인터넷 자료가 틀린 게 아니라 버전이 다른 것이다 |