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>
892 lines
37 KiB
Markdown
892 lines
37 KiB
Markdown
# A-7a 재현 가이드 — DB 에게 직접 물어서 원인을 확정하고, 같은 설정에서 세 가지 답을 본다
|
||
|
||
해설 문서: [`docs/experiment-a7a-volatile-cause.md`](../../experiment-a7a-volatile-cause.md) ·
|
||
증거 원문: [`docs/evidence/a7a-volatile-cause/`](../../evidence/a7a-volatile-cause/)
|
||
|
||
## 이 가이드가 끝나면
|
||
|
||
당신 터미널에서 이것들을 **직접 본다.**
|
||
|
||
| 보게 되는 것 | 어디서 |
|
||
|---|---|
|
||
| 로그인이 SQL 을 **0개** 쏘는 것 | PostgreSQL 문장 로그 |
|
||
| refresh 가 쏘는 **딱 한 문장**의 이름 | 같은 로그 — `CLIENT_SCOPE_CLIENT` |
|
||
| 그 문장이 **첫 refresh 에만** 나오는 것 | 표식 사이 SQL 0건 |
|
||
| A-7 이 지목한 `REVOKED_TOKEN` 이 **한 번도 안 나오는 것** | 같은 로그 |
|
||
| 같은 설정에서 **400 · 500 · 200 셋이 다 나오는 것** | 캐시 온도 세 상태 |
|
||
| 실패한 SQL 을 Keycloak 로그가 **직접 지목하는 것** | `JDBC exception executing SQL [...]` |
|
||
|
||
## 전제
|
||
|
||
- [`05-keycloak`](../05-keycloak/) 이 끝나 있다.
|
||
- **[A-7](a7-volatile-comparison.md) 을 먼저 한다.** 이 실험은 A-7 이 남긴
|
||
가설을 확정하는 것이고, A-7 의 4-4 에서 본 `500` 이 출발점이다.
|
||
- [A-3](a3-database-crash.md) 의 문장 로깅을 해 봤으면 3절이 익숙할 것이다.
|
||
같은 기법이다.
|
||
- 명령은 **`kc-lab-1` 에서** 친다. `kubectl` 은 `sudo` 로 쓴다.
|
||
- 터미널 **두 개**를 열어 두면 편하다. 하나는 표식·요청용, 하나는 로그 관찰용.
|
||
|
||
## 주의 — 주입이 세 개다. 복구도 세 개다
|
||
|
||
1. PostgreSQL **문장 로깅**을 켠다 → 끄지 않으면 다음 실험의 로그가 폭주한다
|
||
2. Keycloak 을 **volatile** 로 바꾼다 → 되돌리지 않으면 A층 결론이 오염된다
|
||
3. PostgreSQL 을 **여러 번 내렸다 올린다** → 마지막에 올라와 있어야 한다
|
||
|
||
**실험대에서만 한다.** 전 구간 약 40분이고, 되돌리는 방법은 매 단계에 적어
|
||
두었다. 중간에 그만두려면 [5. 복구](#5-복구) 를 위에서부터 그대로 친다.
|
||
|
||
## 표시 규약
|
||
|
||
| 표시 | 뜻 |
|
||
|---|---|
|
||
| **실측** | 2026-09-04 11:18–11:24 **UTC** 수집 기록의 **출력 원문**. 증거 파일에 그대로 있다 |
|
||
| **형태** | 값이 매번 달라지는 출력. 모양만 보이고 숫자는 당신 것과 다르다 |
|
||
| **미검증** | 손으로 치기 좋게 이 가이드에서 고친 형태. 원래 실행은 스크립트로 했다 |
|
||
|
||
> **시각이 UTC 다.** 증거 파일의 `11:18:49` 는 KST 로 20:18 이다. 문서 상단의
|
||
> `20:18–20:24 KST` 와 같은 시각이며, **PostgreSQL 컨테이너가 UTC 로 로그를
|
||
> 찍기 때문**이다. 로그 시각과 `date` 를 비교할 때 이걸 잊으면 9시간을 헤맨다.
|
||
|
||
UUID·IP·파드 이름은 **당신 환경에서 다르다.** 이 문서는 자리표시자(`<...>`)를
|
||
쓰지 않는 대신, 그 값을 뽑는 명령을 먼저 적는다.
|
||
|
||
---
|
||
|
||
# 0. 왜 이 실험을 하는가
|
||
|
||
A-7 은 이렇게 끝났다.
|
||
|
||
> **측정은 확실하지만 원인은 확정하지 못했다.** 유력한 후보는 `REVOKED_TOKEN`
|
||
> 테이블이다 — refresh token 회전에서 **이미 쓴 토큰인지** 확인하려면 그 테이블을
|
||
> 봐야 하고, 그 경로는 캐시되지 않는다.
|
||
|
||
**그럴듯하다. 그리고 틀렸다.**
|
||
|
||
```
|
||
가설을 세우는 것 → 괜찮다
|
||
가설을 표에 적는 것 → 다음 사람이 사실로 읽는다
|
||
확정하는 방법이 있는데 안 하는 것 → 이 실험이 고치는 것
|
||
```
|
||
|
||
「refresh 가 어느 테이블 때문에 실패하는가」는 **추측으로 답할 문제가 아니다.**
|
||
Keycloak 소스를 읽는 대신 **DB 가 실제로 받은 문장**을 보면 된다.
|
||
|
||
그리고 확정해 보니 원인만 틀린 게 아니었다. **A-7 의 표 자체가 조건부였다.**
|
||
같은 설정에서 캐시 온도만으로 답이 셋으로 갈린다. **한 번 재고 표로 적으면
|
||
안 되는 종류의 측정**이었던 것이다.
|
||
|
||
---
|
||
|
||
# 1. 기준선 — 켜기 전에 지금 상태를 본다
|
||
|
||
넓은 것부터 좁혀 간다.
|
||
|
||
```
|
||
파드 → 문장 로깅이 꺼져 있나 → args → 탐침 파드 → 로그가 지금 무엇으로 차 있나
|
||
```
|
||
|
||
## 1-1. 파드
|
||
|
||
**확인**
|
||
```bash
|
||
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
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 셋 다 `Running`. **`postgres` 가 있어야 한다** —
|
||
이 실험은 그것을 내렸다 올렸다 한다.
|
||
|
||
## 1-2. 문장 로깅이 꺼져 있나
|
||
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "show log_statement"
|
||
```
|
||
**형태**
|
||
```
|
||
log_statement
|
||
---------------
|
||
none
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `none`.
|
||
|
||
**이 결과가 의미하는 것** — 앞 실험이 켜 둔 채 끝내지 않았다. `all` 이면
|
||
**누가 켜 두었는지 모르는 상태**이고, 그대로 진행하면 지금 쌓인 로그가 어느
|
||
실험 것인지 구별할 수 없다. 그때는 먼저 끄고, 로그가 한 바퀴 돌 때까지 기다린다.
|
||
|
||
## 1-3. 지금 args 가 무엇인가
|
||
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab get statefulset keycloak \
|
||
-o jsonpath='{.spec.template.spec.containers[0].args}' ; echo
|
||
```
|
||
**형태**
|
||
```
|
||
["start"]
|
||
```
|
||
|
||
**이 값을 적어 둔다.** 5-3 에서 이대로 되돌린다.
|
||
|
||
## 1-4. 탐침 파드
|
||
|
||
Keycloak 컨테이너에는 `curl` 도 `wget` 도 없다(`exit 127`). 탐침 파드를 띄운다.
|
||
**이 실험은 Keycloak 을 여러 번 재시작하므로 탐침은 반드시 StatefulSet 밖에
|
||
있어야 한다.**
|
||
|
||
**하기**
|
||
```bash
|
||
K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||
sudo kubectl -n keycloak-lab run a7a-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 7200
|
||
sudo kubectl -n keycloak-lab wait --for=condition=Ready pod/a7a-probe --timeout=120s
|
||
```
|
||
|
||
**되돌리기**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab delete pod a7a-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`
|
||
>
|
||
> **★ 명령줄에 평문 비밀번호를 쓰지 않는다.** 원래 실험의 재현 절차에는 그대로
|
||
> 적혀 있는데, **파드 안 `ps` 에도 셸 히스토리에도 남는다.**
|
||
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec a7a-probe -- sh -c 'echo "K0=$K0 PW길이=${#PW}"'
|
||
```
|
||
**형태**
|
||
```
|
||
K0=10.42.1.94 PW길이=19
|
||
```
|
||
|
||
## 1-5. 로그가 지금 무엇으로 차 있나 — **이걸 알아야 걸러 낼 수 있다**
|
||
|
||
**확인** — 로깅을 켜기 전에 한 번 본다
|
||
```bash
|
||
sudo kubectl -n keycloak-lab logs deploy/postgres --tail=20
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 조용하다. 여기까지는 에러만 찍힌다.
|
||
|
||
**이 결과가 의미하는 것** — 로그가 조용한 것이 기준선이다. 다음 절에서 켜면
|
||
**JGroups 가 5초마다 하는 `JGROUPS_PING` 폴링**이 로그를 계속 채운다. 그것이
|
||
소음이고, 4절에서 `grep -v JGROUPS_PING` 으로 거른다. **소음을 먼저 봐 두면
|
||
거르는 이유를 안다.**
|
||
|
||
---
|
||
|
||
# 2. 주입 — 두 개를 순서대로 넣는다
|
||
|
||
## 2-1. 주입 ① PostgreSQL 문장 로깅
|
||
|
||
### 개념 — 문장 로깅은 무엇인가
|
||
|
||
**무엇인가.** `log_statement = 'all'` 을 켜면 서버가 받은 **모든 SQL** 을 로그에
|
||
찍는다. 애플리케이션을 고치지 않고 **「이 요청이 DB 를 어떻게 쓰는지」** 를
|
||
밖에서 볼 수 있다.
|
||
|
||
**왜 여기 나오나.** 「refresh 가 어느 테이블 때문에 실패하는가」를 확정하려면
|
||
DB 가 실제로 받은 문장을 봐야 한다. Keycloak 안을 들여다볼 필요가 없다.
|
||
|
||
**없거나 틀리면.** 여기서 정확히 A-7 이 겪은 일이 벌어진다 — 그럴듯한 테이블
|
||
이름을 골라 가설로 적게 되고, **그게 틀려도 아무도 모른다.**
|
||
|
||
**되돌리기** — 먼저 읽어 둔다
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "alter system reset log_statement" -c "select pg_reload_conf()"
|
||
```
|
||
|
||
**하기**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "alter system set log_statement='all'" -c "select pg_reload_conf()"
|
||
```
|
||
|
||
**확인** — 실제로 켜졌나
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "show log_statement"
|
||
```
|
||
**형태**
|
||
```
|
||
log_statement
|
||
---------------
|
||
all
|
||
```
|
||
|
||
`none` 이면 `pg_reload_conf()` 가 안 돈 것이다. **`alter system` 은
|
||
`postgresql.auto.conf` 에 쓸 뿐이고 reload 를 해야 적용된다.**
|
||
|
||
**확인** — 로그가 실제로 차기 시작했나
|
||
```bash
|
||
sudo kubectl -n keycloak-lab logs deploy/postgres --tail=10
|
||
```
|
||
**형태**
|
||
```
|
||
2026-09-04 11:17:40.112 UTC [214] LOG: execute <unnamed>: select ... from JGROUPS_PING ...
|
||
```
|
||
|
||
**어디를 봐야 하는가** — **`JGROUPS_PING` 이 계속 나온다.** 1-5 에서 예고한 소음이다.
|
||
이게 안 보이면 로깅이 안 켜진 것이다.
|
||
|
||
## 2-2. 주입 ② volatile 전환
|
||
|
||
**되돌리기** — 먼저 읽어 둔다
|
||
```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
|
||
```
|
||
|
||
**하기**
|
||
```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"]}]'
|
||
sudo kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||
```
|
||
|
||
**빌드 옵션이라 기동 시 재빌드가 일어나 오래 걸린다.** `--timeout=500s` 를 주는
|
||
이유다.
|
||
|
||
**★ 파드 IP 가 바뀌었다.** 탐침 파드를 다시 띄운다.
|
||
```bash
|
||
sudo kubectl -n keycloak-lab delete pod a7a-probe --ignore-not-found
|
||
K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||
sudo kubectl -n keycloak-lab run a7a-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 7200
|
||
sudo kubectl -n keycloak-lab wait --for=condition=Ready pod/a7a-probe --timeout=120s
|
||
```
|
||
|
||
---
|
||
|
||
# 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 exec a7a-probe -- sh -c \
|
||
'curl -s -o /dev/null -w "%{http_code}\n" -X POST \
|
||
"http://$K0:8080/realms/master/protocol/openid-connect/token" \
|
||
-d grant_type=password -d client_id=admin-cli \
|
||
-d username=admin -d "password=$PW"'
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-tAc "select count(*) from offline_user_session where offline_flag='0'"
|
||
```
|
||
**실측** — [`01-cause-determined.txt`](../../evidence/a7a-volatile-cause/01-cause-determined.txt)
|
||
```
|
||
volatile 전환 확인
|
||
args: ["start","--features-disabled=persistent-user-sessions"]
|
||
로그인 200 · offline_user_session 행수 = 0 ← volatile 맞다
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 세 가지가 다 맞아야 한다. 로그인이 `200` 인데 **행이
|
||
안 생기는 것**이 volatile 의 증거다.
|
||
|
||
> 행 수가 0 이 아니면 옛 행이 남아 있는 것이다. A-7 의 2-1 처럼
|
||
> `delete from offline_user_session` 을 먼저 하고 다시 잰다.
|
||
|
||
## 3-2. 문장 로그가 지금 요청을 잡고 있나
|
||
|
||
**확인** — 방금 로그인 직후에 친다
|
||
```bash
|
||
sudo kubectl -n keycloak-lab logs deploy/postgres --since=60s | tail -20
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `JGROUPS_PING` 말고 다른 것이 섞여 있는지.
|
||
|
||
**이 결과가 의미하는 것** — 이 시점에서는 **거의 `JGROUPS_PING` 뿐일 것**이다.
|
||
그게 이 실험의 첫 발견인데, 지금은 「내 요청이 어디 있는지 모르겠다」로만 보인다.
|
||
**구간을 나눠야 볼 수 있다.** 그게 다음 절이다.
|
||
|
||
---
|
||
|
||
# 4. 효과를 관찰한다
|
||
|
||
## 4-1. 표식으로 구간을 나눈다
|
||
|
||
로그는 `JGROUPS_PING` 폴링으로 계속 채워진다. 어느 문장이 로그인이고 어느 것이
|
||
refresh 인지 가르려면 **경계를 찍어야 한다.**
|
||
|
||
**개념** — `psql` 로 아무 `select` 나 보내면 **그 문장 자체가 로그에 남는다.**
|
||
그러면 리터럴 문자열이 로그 안의 이정표가 된다.
|
||
|
||
**하기** — 표식 하나를 넣어 본다
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-tAc "select 'MARK_TEST'"
|
||
```
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab logs deploy/postgres --tail=5 | grep MARK_TEST
|
||
```
|
||
**형태**
|
||
```
|
||
2026-09-04 11:18:40.102 UTC [301] LOG: statement: select 'MARK_TEST'
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `statement: select 'MARK_TEST'` 가 보이는 것.
|
||
안 보이면 로깅이 안 켜졌다(2-1 로 돌아간다).
|
||
|
||
**이 결과가 의미하는 것** — 이제 **표식과 표식 사이만 잘라 볼 수 있다.**
|
||
|
||
## 4-2. 로그인이 무슨 SQL 을 쏘는가
|
||
|
||
**하기** — 표식 → 로그인 → 표식. **세 명령을 붙여서 친다**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-tAc "select 'MARK_LOGIN_START'"
|
||
sudo kubectl -n keycloak-lab exec a7a-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"'
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-tAc "select 'MARK_LOGIN_END'"
|
||
```
|
||
**형태**
|
||
```
|
||
rt 1188 bytes
|
||
```
|
||
|
||
**★ `1 bytes` 면 파싱이 실패한 것이다.** 그 상태로 4-3 을 하면 빈 토큰을 보내고
|
||
엉뚱한 오류를 보게 된다. `cat /tmp/tok` 으로 본문을 본다.
|
||
|
||
**확인** — 표식 사이를 잘라 본다
|
||
```bash
|
||
sudo kubectl -n keycloak-lab logs deploy/postgres --tail=4000 > /tmp/pg.log
|
||
awk '/MARK_LOGIN_START/,/MARK_LOGIN_END/' /tmp/pg.log | grep -v JGROUPS_PING
|
||
```
|
||
|
||
**실측** — [`01-cause-determined.txt`](../../evidence/a7a-volatile-cause/01-cause-determined.txt)
|
||
```
|
||
11:18:49.461 statement: select 'MARK_LOGIN_START'
|
||
11:18:49.743 statement: select 'MARK_LOGIN_END'
|
||
↑ 사이에 아무것도 없다
|
||
```
|
||
|
||
**어디를 봐야 하는가** — **두 줄뿐이다.**
|
||
|
||
**이 결과가 의미하는 것** — **로그인은 SQL 을 0개 쏜다.** realm·사용자·클라이언트가
|
||
전부 Infinispan 캐시에 있고, volatile 이라 세션 쓰기도 없다. DB 없이 완결된다 —
|
||
A-7 이 적은 그대로다.
|
||
|
||
> `awk '/A/,/B/'` 는 **A 가 나온 줄부터 B 가 나온 줄까지** 출력한다. 로그를 구간으로
|
||
> 자를 때 이보다 짧게 쓰는 방법은 없다. 파일로 먼저 받는 것은 같은 로그를 여러
|
||
> 구간으로 반복해서 잘라 볼 것이기 때문이다.
|
||
|
||
## 4-3. ★ refresh 는 딱 한 문장을 쏜다 — 그리고 가설이 지목한 것이 아니다
|
||
|
||
**하기** — 표식 → refresh → 표식
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-tAc "select 'MARK_REFRESH_START'"
|
||
sudo kubectl -n keycloak-lab exec a7a-probe -- sh -c \
|
||
'curl -s -o /dev/null -w "%{http_code}\n" -X POST \
|
||
"http://$K0:8080/realms/master/protocol/openid-connect/token" \
|
||
-d grant_type=refresh_token -d client_id=admin-cli \
|
||
-d "refresh_token=$(cat /tmp/rt)"'
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-tAc "select 'MARK_REFRESH_END'"
|
||
```
|
||
**형태**
|
||
```
|
||
200
|
||
```
|
||
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab logs deploy/postgres --tail=4000 > /tmp/pg.log
|
||
awk '/MARK_REFRESH_START/,/MARK_REFRESH_END/' /tmp/pg.log | grep -v JGROUPS_PING
|
||
```
|
||
|
||
**실측** — [`01-cause-determined.txt`](../../evidence/a7a-volatile-cause/01-cause-determined.txt)
|
||
```
|
||
11:18:52.009 statement: select 'MARK_REFRESH_START'
|
||
11:18:52.137 statement: BEGIN
|
||
11:18:52.137 execute <unnamed>/C_107:
|
||
select cscme1_0.SCOPE_ID from CLIENT_SCOPE_CLIENT cscme1_0
|
||
where cscme1_0.CLIENT_ID=$1 and cscme1_0.DEFAULT_SCOPE=$2
|
||
parameters: $1 = '131a9912-b578-4b9c-b16a-97518704077e', $2 = 'f'
|
||
11:18:52.148 execute S_2: COMMIT
|
||
11:18:52.253 statement: select 'MARK_REFRESH_END'
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 세 가지다.
|
||
|
||
- **문장이 하나뿐이다.** `BEGIN` / `COMMIT` 사이에 `select` 한 개
|
||
- 테이블 이름이 **`CLIENT_SCOPE_CLIENT`** 다
|
||
- `parameters` 줄의 **`$2 = 'f'`**
|
||
|
||
**확인** — 가설이 지목한 테이블이 정말 없는지 직접 센다
|
||
```bash
|
||
awk '/MARK_REFRESH_START/,/MARK_REFRESH_END/' /tmp/pg.log | grep -ci revoked_token
|
||
```
|
||
**실측** — [`01-cause-determined.txt`](../../evidence/a7a-volatile-cause/01-cause-determined.txt)
|
||
```
|
||
REVOKED_TOKEN 은 **한 번도 나오지 않는다.**
|
||
```
|
||
|
||
**이 결과가 의미하는 것** — **A-7 의 가설은 틀렸다.** 그럴듯했지만 로그가
|
||
아니라고 말한다. 그리고 이제 **로그가 지목하는 문장**이 있다.
|
||
|
||
### 개념 — `DEFAULT_SCOPE='f'` 가 무슨 뜻인가
|
||
|
||
**무엇인가.** Keycloak 의 클라이언트는 스코프를 두 종류로 갖는다.
|
||
|
||
| | 뜻 | `DEFAULT_SCOPE` |
|
||
|---|---|---|
|
||
| default scope | 항상 붙는다 | `t` |
|
||
| **optional scope** | **요청이 `scope=` 로 달라고 해야 붙는다** | **`f`** |
|
||
|
||
**왜 여기 나오나.** refresh 는 **새 access token 을 만든다.** 그 토큰에 어떤
|
||
스코프를 담을지 정하려면 「이 클라이언트가 요청 가능한 optional 스코프가
|
||
무엇인가」를 알아야 한다. 그 목록이 `CLIENT_SCOPE_CLIENT` 에 있다. **로그인
|
||
때는 이미 결정된 것을 쓰지만, refresh 는 다시 계산한다.**
|
||
|
||
**없거나 틀리면.** 이 조회가 실패하면 토큰을 만들 수 없어 **500** 이다.
|
||
`400 Session not active` 와 달리 **세션 문제가 아니다** — 그래서 A-7 이 세션 계열
|
||
테이블(`REVOKED_TOKEN`)을 의심한 것이 자연스러웠지만 틀렸다.
|
||
|
||
**확인** — 그 UUID 가 어느 클라이언트인지 궁금하면 물어본다
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "select id, client_id from client where id='131a9912-b578-4b9c-b16a-97518704077e'"
|
||
```
|
||
|
||
**당신 환경에서는 UUID 가 다르다.** 위 로그의 `$1` 값을 그대로 넣는다.
|
||
`admin-cli` 가 나오면 방금 친 요청의 클라이언트가 맞다.
|
||
|
||
## 4-4. 그 조회는 한 번뿐이다 — 여기서 표가 흔들리기 시작한다
|
||
|
||
**하기** — refresh 를 연속 3회. 사이사이 표식을 넣는다
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-tAc "select 'MARK_R1'"
|
||
sudo kubectl -n keycloak-lab exec a7a-probe -- sh -c \
|
||
'curl -s -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)" > /tmp/tok
|
||
sed -n "s/.*\"refresh_token\":\"\([^\"]*\)\".*/\1/p" /tmp/tok > /tmp/rt
|
||
echo "rt $(wc -c < /tmp/rt) bytes"'
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-tAc "select 'MARK_R2'"
|
||
```
|
||
|
||
**★ 매번 `/tmp/rt` 를 다시 채운다.** refresh token 은 회전한다. 옛 것을 계속 쓰면
|
||
나오는 오류가 **무효화 때문인지 재사용 때문인지 구별되지 않는다.**
|
||
|
||
같은 모양으로 `MARK_R3` · `MARK_R_END` 까지 두 번 더 한다.
|
||
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab logs deploy/postgres --tail=4000 > /tmp/pg.log
|
||
awk '/MARK_R1/,/MARK_R_END/' /tmp/pg.log | grep -v JGROUPS_PING
|
||
```
|
||
|
||
**실측** — [`01-cause-determined.txt`](../../evidence/a7a-volatile-cause/01-cause-determined.txt)
|
||
```
|
||
연속 refresh 3회, 전부 200. 표식 사이 SQL:
|
||
statement: select 'MARK_R1'
|
||
statement: select 'MARK_R2'
|
||
statement: select 'MARK_R3'
|
||
statement: select 'MARK_R_END'
|
||
↑ SQL 0건
|
||
```
|
||
|
||
**어디를 봐야 하는가** — **표식 네 줄만 있고 그 사이에 아무것도 없다.**
|
||
|
||
**이 결과가 의미하는 것** — **첫 refresh 가 캐시를 채우고, 이후로는 DB 를 보지
|
||
않는다.** 그러면 이런 질문이 따라온다.
|
||
|
||
> **DB 를 언제 내리느냐에 따라 답이 달라지는 것 아닌가?**
|
||
|
||
그렇다. 그게 다음 절이다.
|
||
|
||
## 4-5. ★ 같은 설정에서 답이 셋으로 갈린다 — 셋 다 재현한다
|
||
|
||
| 캐시 상태 | 로그인 | refresh | 실패한 SQL |
|
||
|---|---|---|---|
|
||
| **완전 냉시동** (재시작 직후) | **400** | 400 | `select ce1_0.ID from CLIENT where CLIENT_ID=? and REALM_ID=?` |
|
||
| **CLIENT 만 더움** ← A-7 이 본 것 | 200 | **500** | `select cscme1_0.SCOPE_ID from CLIENT_SCOPE_CLIENT …` |
|
||
| **완전히 더움** | 200 | **200** | 없음 (SQL 0건) |
|
||
|
||
**★ 한 번만 재고 넘어가면 반드시 틀린 표를 쓰게 된다.** A-7 이 그렇게 했다.
|
||
셋 다 재현해야 한다.
|
||
|
||
**되돌리기** — 세 재현 모두 공통이다. 어느 단계에서 멈추든 이것부터
|
||
```bash
|
||
sudo kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||
sudo kubectl -n keycloak-lab rollout status deployment/postgres --timeout=180s
|
||
```
|
||
|
||
### 캐시를 식히는 방법 — **Keycloak 재시작이 유일하다**
|
||
|
||
```
|
||
Infinispan 캐시 = 프로세스 메모리
|
||
│
|
||
└─ 파드가 살아 있는 한 안 식는다
|
||
└─ 그래서 세 재현 사이마다 rollout restart 를 한다
|
||
```
|
||
|
||
**이 재시작을 건너뛰면 세 상태가 하나로 뭉개진다.** 이미 더워진 캐시에서 계속
|
||
재게 되므로 **A·B 를 재도 C 의 답(200/200)이 나오고**, 「A-7 이 틀렸다」는 엉뚱한
|
||
결론에 도달한다.
|
||
|
||
### 재현 A — 완전 냉시동이면 로그인부터 400
|
||
|
||
**하기**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
||
sudo kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||
sudo kubectl -n keycloak-lab scale deployment/postgres --replicas=0
|
||
sudo kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s
|
||
```
|
||
|
||
**★ 재시작과 DB 정지 사이에 아무 요청도 보내지 않는다.** 한 번이라도 로그인하면
|
||
캐시가 더워져서 이건 재현 B 가 된다.
|
||
|
||
파드 IP 가 바뀌었으므로 탐침을 다시 띄운다.
|
||
```bash
|
||
sudo kubectl -n keycloak-lab delete pod a7a-probe --ignore-not-found
|
||
K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||
sudo kubectl -n keycloak-lab run a7a-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 7200
|
||
sudo kubectl -n keycloak-lab wait --for=condition=Ready pod/a7a-probe --timeout=120s
|
||
```
|
||
|
||
**확인** — 로그인. **본문까지 본다**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec a7a-probe -- sh -c \
|
||
'curl -s -w "\n%{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"'
|
||
```
|
||
**실측** — [`01-cause-determined.txt`](../../evidence/a7a-volatile-cause/01-cause-determined.txt)
|
||
```
|
||
로그인 400 {"error":"unauthorized_client",
|
||
"error_description":"Unexpected error when authenticating client"}
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `unauthorized_client`. **`invalid_grant` 가 아니다.**
|
||
세션 문제가 아니라 **클라이언트를 못 찾은 것**이다.
|
||
|
||
**확인** — 왜인지는 Keycloak 로그가 직접 말한다
|
||
```bash
|
||
sudo kubectl -n keycloak-lab logs keycloak-0 --tail=150 \
|
||
| grep -oE 'JDBC exception executing SQL \[[^]]*\] \[[^]]*\]'
|
||
```
|
||
**실측** — 같은 파일
|
||
```
|
||
ERROR [org.keycloak.services] KC-SERVICES0015: Unexpected error when
|
||
authenticating client: org.hibernate.exception.GenericJDBCException:
|
||
JDBC exception executing SQL [FATAL: terminating connection due to
|
||
administrator command]
|
||
[select ce1_0.ID from CLIENT ce1_0 where ce1_0.CLIENT_ID=? and ce1_0.REALM_ID=?]
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 대괄호가 **두 쌍**이다. 앞은 **DB 가 준 오류**,
|
||
뒤는 **실패한 SQL 원문**. `grep -oE` 로 그 두 쌍만 뽑는 이유가 이것이다.
|
||
|
||
> 아무것도 안 나오면 `--tail` 을 늘리거나 `grep -i 'JDBC exception'` 으로 먼저
|
||
> 넓게 본다. 정규식이 안 맞는 것과 로그에 없는 것은 다르다.
|
||
|
||
**이 결과가 의미하는 것** — **A-7 은 「volatile 이면 DB 없이 로그인된다」고 적었다.
|
||
냉시동에서는 아니다.** 클라이언트 조회조차 캐시에 없기 때문이다.
|
||
|
||
### 재현 B — A-7 이 본 그 조건
|
||
|
||
**하기** — DB 를 살리고, 재시작하고, **로그인만 한 번** 하고, DB 를 내린다
|
||
```bash
|
||
sudo kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||
sudo kubectl -n keycloak-lab rollout status deployment/postgres --timeout=180s
|
||
sudo kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
||
sudo kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||
```
|
||
|
||
탐침을 새 IP 로 다시 띄운 뒤(재현 A 와 같은 명령), **로그인 한 번만** 한다.
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec a7a-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"'
|
||
```
|
||
|
||
**★ 여기서 refresh 를 하면 안 된다.** 하는 순간 재현 C 가 된다.
|
||
|
||
```bash
|
||
sudo kubectl -n keycloak-lab scale deployment/postgres --replicas=0
|
||
sudo kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s
|
||
```
|
||
|
||
**확인** — 이제 refresh
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec a7a-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)"'
|
||
```
|
||
**실측** — [`01-cause-determined.txt`](../../evidence/a7a-volatile-cause/01-cause-determined.txt)
|
||
```
|
||
로그인 200
|
||
refresh 500 {"error":"unknown_error"}
|
||
```
|
||
|
||
**확인** — 실패한 SQL
|
||
```bash
|
||
sudo kubectl -n keycloak-lab logs keycloak-0 --tail=150 \
|
||
| grep -oE 'JDBC exception executing SQL \[[^]]*\] \[[^]]*\]'
|
||
```
|
||
**실측** — 같은 파일
|
||
```
|
||
JDBC exception executing SQL [FATAL: terminating connection due to
|
||
administrator command]
|
||
[select cscme1_0.SCOPE_ID from CLIENT_SCOPE_CLIENT cscme1_0
|
||
where cscme1_0.CLIENT_ID=? and cscme1_0.DEFAULT_SCOPE=?]
|
||
```
|
||
|
||
**어디를 봐야 하는가** — **4-3 에서 본 그 문장이다.** 문장 로깅이 「이 문장을
|
||
쏜다」를 보여줬고, 여기서는 「이 문장이 실패했다」를 보여준다. **두 개가 만나면
|
||
가설이 아니라 확정이다.**
|
||
|
||
**`500 unknown_error` 인 이유도 이제 안다.** 세션은 멀쩡하다. 토큰을 조립하다가
|
||
DB 가 없어서 못 만든 것이고, Keycloak 은 그걸 사용자 오류로 분류할 방법이 없어서
|
||
`unknown_error` 를 준다.
|
||
|
||
### 재현 C — 완전히 더우면 둘 다 200
|
||
|
||
**하기** — DB 를 살리고, 재시작하고, **refresh 를 3회 미리 돌린 뒤** DB 를 내린다
|
||
```bash
|
||
sudo kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||
sudo kubectl -n keycloak-lab rollout status deployment/postgres --timeout=180s
|
||
sudo kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
||
sudo kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||
```
|
||
|
||
탐침을 새 IP 로 다시 띄운 뒤, 로그인 1회 + refresh 3회(4-4 와 같은 형태로
|
||
`/tmp/rt` 를 매번 갱신하며).
|
||
|
||
```bash
|
||
sudo kubectl -n keycloak-lab scale deployment/postgres --replicas=0
|
||
sudo kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s
|
||
```
|
||
|
||
**확인** — 로그인과 refresh 를 둘 다
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec a7a-probe -- sh -c \
|
||
'curl -s -o /dev/null -w "login %{http_code}\n" -X POST \
|
||
"http://$K0:8080/realms/master/protocol/openid-connect/token" \
|
||
-d grant_type=password -d client_id=admin-cli -d "password=$PW" -d username=admin
|
||
curl -s -o /dev/null -w "refresh %{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)"'
|
||
```
|
||
**실측** — [`01-cause-determined.txt`](../../evidence/a7a-volatile-cause/01-cause-determined.txt)
|
||
```
|
||
refresh 를 3회 미리 돌려 캐시를 채운 뒤 postgres 정지
|
||
로그인 200
|
||
refresh 200 ← A-7 의 표와 정반대다
|
||
```
|
||
|
||
**이 결과가 의미하는 것** — **같은 설정, 같은 명령, 세 개의 답.** 무엇이 다른지는
|
||
`kubectl get` 어디에도 안 나온다. **캐시 온도는 보이지 않는 상태다.**
|
||
|
||
```
|
||
volatile + DB 정지의 결과
|
||
= "무엇을 하느냐"가 아니라
|
||
"그 경로가 이미 캐시를 채웠느냐"
|
||
```
|
||
|
||
> **A-1 에서 conntrack 이 「주입했는데 안 걸렸다」를 만든 것과 같은 계열의
|
||
> 함정이다.** 상태가 결과를 바꾸는데 그 상태가 안 보인다.
|
||
|
||
> **persistent(기본값)에는 해당하지 않는다.** 세션 자체를 DB 에 쓰므로 DB 가
|
||
> 없으면 캐시 온도와 무관하게 실패한다. **이 조건부성은 volatile 고유의
|
||
> 성질**이고, 옛 방식이 「DB 의존이 적다」고 말할 때 놓치는 부분이다.
|
||
|
||
---
|
||
|
||
# 5. 복구
|
||
|
||
**세 개를 순서대로 되돌린다.** 순서가 있다 — DB 가 살아 있어야 나머지가 된다.
|
||
|
||
## 5-1. PostgreSQL 을 되살린다
|
||
|
||
**하기**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||
sudo kubectl -n keycloak-lab wait --for=condition=Ready pod -l app=postgres --timeout=180s
|
||
```
|
||
|
||
## 5-2. ★ 문장 로깅을 끈다 — 잊으면 다음 실험이 전부 오염된다
|
||
|
||
**하기**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "alter system reset log_statement" -c "select pg_reload_conf()"
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "show log_statement"
|
||
```
|
||
**형태**
|
||
```
|
||
log_statement
|
||
---------------
|
||
none
|
||
```
|
||
|
||
**왜 급한가** — [A-3](a3-database-crash.md) 은 수백 건의 로그인을 최대한 빨리
|
||
돈다. `log_statement='all'` 이면 **로그인 하나에 SQL 열 몇 줄씩** 쌓인다.
|
||
로그가 폭주하고, 디스크 I/O 가 늘어 **크래시 타이밍 자체가 달라진다.**
|
||
즉 **다음 실험의 측정값이 이 설정 때문에 바뀐다.**
|
||
|
||
## 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
|
||
```
|
||
|
||
## 5-4. 정말 persistent 로 돌아왔는지 — 동작으로 확인한다
|
||
|
||
**args 문자열만 보고 끝내지 않는다.**
|
||
|
||
**하기** — 탐침을 새 IP 로 띄우고 로그인 한 번
|
||
```bash
|
||
sudo kubectl -n keycloak-lab delete pod a7a-probe --ignore-not-found
|
||
K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||
sudo kubectl -n keycloak-lab run a7a-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/a7a-probe --timeout=120s
|
||
sudo kubectl -n keycloak-lab exec a7a-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 exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-tAc "select count(*) from offline_user_session where offline_flag='0'"
|
||
```
|
||
|
||
**어디를 봐야 하는가** — **0 이 아니어야 한다.** 로그인 후 행이 생기면 persistent 다.
|
||
원래 재현 절차가 마지막에 이 한 줄을 두는 이유가 이것이다.
|
||
|
||
## 5-5. 원상복구 확인표
|
||
|
||
| 항목 | 명령 | 돌아왔을 때 |
|
||
|---|---|---|
|
||
| **문장 로깅** | `psql -c "show log_statement"` | **`none`** |
|
||
| args | `get statefulset keycloak -o jsonpath='{...containers[0].args}'` | `["start"]` |
|
||
| DB | `sudo kubectl -n keycloak-lab get pods -l app=postgres` | `1/1 Running` |
|
||
| **동작** | 위 5-4 | 로그인 후 세션 행이 **생긴다** |
|
||
| 파드 | `sudo kubectl -n keycloak-lab get pods -o wide` | `keycloak` 둘 다 `1/1 Running` |
|
||
| 클러스터 | `vendor_cluster_size` | 양쪽 `2` |
|
||
| 탐침 파드 | `sudo kubectl -n keycloak-lab get pod a7a-probe` | `NotFound` |
|
||
| 임시 파일 | `ls /tmp/pg.log` | 지워도 된다 |
|
||
| 밖 | `curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master` | `200` |
|
||
|
||
```bash
|
||
sudo kubectl -n keycloak-lab delete pod a7a-probe --ignore-not-found
|
||
rm -f /tmp/pg.log
|
||
```
|
||
|
||
> **이 실험이 재지 않은 것** — 캐시가 「얼마나 오래」 더운지는 재지 않았다.
|
||
> `CLIENT_SCOPE_CLIENT` 결과의 캐시 만료 시간을 모르므로, **한참 뒤에 다시 재면
|
||
> 또 다른 답이 나올 수도 있다.** 그것까지 확인하려면 재현 C 뒤에 시간을 두고
|
||
> 같은 시험을 반복해야 한다.
|
||
|
||
---
|
||
|
||
# 막히면
|
||
|
||
전부 이 실험대가 **실제로 겪은** 증상이다. 지어낸 것은 없다.
|
||
|
||
| 증상 | 원인 | 확인 |
|
||
|---|---|---|
|
||
| 표식이 로그에 안 보인다 | `pg_reload_conf()` 를 안 했다 | `show log_statement` 가 `all` 인지 — 2-1 |
|
||
| 표식 사이가 `JGROUPS_PING` 으로 가득하다 | 정상이다. 5초마다 폴링한다 | `grep -v JGROUPS_PING` — 4-2 |
|
||
| 표식이 두 번 나온다 | 로그를 여러 번 받아 구간이 겹쳤다 | `--tail` 을 줄이거나 새 표식 이름을 쓴다 |
|
||
| 로그 시각이 9시간 어긋난다 | **컨테이너 로그가 UTC 다** | `date -u` 와 비교한다 |
|
||
| refresh 가 `400 Session not active` | 옛 refresh token 을 재사용했다 | 매번 `/tmp/rt` 를 갱신 — 4-4 |
|
||
| `rt 1 bytes` | 파싱 실패. 빈 토큰을 보내게 된다 | `cat /tmp/tok` 으로 본문 확인 — 4-2 |
|
||
| 세 재현이 전부 `200/200` | **재시작을 건너뛰어 캐시가 계속 더웠다** | 재현마다 `rollout restart` — 4-5 |
|
||
| 재현 A 가 `200` 이 나온다 | 재시작 후 요청을 한 번이라도 보냈다 | 재시작 → **바로** DB 정지 |
|
||
| 재현 B 가 `200/200` | 로그인 뒤 refresh 를 미리 했다 | 로그인 **한 번만** 하고 DB 정지 |
|
||
| `JDBC exception` grep 이 빈 출력 | `--tail` 이 짧거나 정규식이 안 맞는다 | `grep -i 'JDBC exception'` 으로 먼저 넓게 |
|
||
| 재시작 뒤 아무 데도 안 닿는다 | **파드 IP 가 바뀌었다** | 탐침을 지우고 새 IP 로 다시 띄운다 |
|
||
| `kubectl exec keycloak-0 -- curl` 이 `exit 127` | Keycloak 이미지에 curl 도 wget 도 없다 | 탐침 파드를 쓴다 |
|
||
| 다음 실험의 postgres 로그가 폭주한다 | **문장 로깅을 끄지 않았다** | `show log_statement` 가 `none` — 5-2 |
|
||
| 다음 실험의 세션이 안 살아남는다 | **volatile 로 둔 채 끝냈다** | 5-4 의 행 수 확인 |
|
||
|
||
---
|
||
|
||
# 왜 이 가이드는 표식을 손으로 넣게 하나
|
||
|
||
원래 실행은 표식을 셸 함수로 감쌌다.
|
||
|
||
```bash
|
||
m() { kubectl -n keycloak-lab exec deploy/postgres -- \
|
||
psql -U keycloak -d keycloak -tAc "select 'MARK_$1'" >/dev/null; }
|
||
```
|
||
|
||
짧고 편하다. 그런데 **출력을 `/dev/null` 로 버린다.** 표식이 실제로 로그에
|
||
들어갔는지 확인하지 않고 다음 명령으로 넘어간다는 뜻이다. 로깅이 안 켜져
|
||
있었다면 **표식 없는 로그를 한참 뒤에 `awk` 로 자르다가** 알게 된다.
|
||
|
||
이 가이드는 표식을 **한 줄씩 손으로** 넣는다. 느리지만 그 자리에서 보이고,
|
||
안 보이면 그 자리에서 안다.
|
||
|
||
---
|
||
|
||
# 다음
|
||
|
||
| 실험 | A-7a 가 남긴 것 |
|
||
|---|---|
|
||
| [A-7](a7-volatile-comparison.md) volatile 비교 | **그 표에 조건을 붙여야 한다.** 「로그인 200 · refresh 500」은 캐시가 반쯤 더울 때만 참이다 |
|
||
| [A-3](a3-database-crash.md) DB 크래시 | 같은 문장 로깅 기법. **RPO 를 재는 데 쓴다** |
|
||
| [A-2](a2-database-loss.md) DB 정지 | persistent 에서는 캐시 온도와 무관하게 실패한다 — 대조군 |
|
||
| 전부 | **한 번 재고 표로 적으면 안 되는 종류가 있다.** 상태가 결과를 바꾸는데 그 상태가 안 보일 때 |
|