fix(setup): 실험대를 새로 세워 setup 35편을 밟고 어긋난 명령과 결과를 고친다
기반 가이드 7단계로 실험대를 철거하고 다시 세운 뒤 virtualization setup 9편과 keycloak-session-store 26편을 순서대로 밟았다. 24편은 끝까지, 11편은 되는 데까지 밟았고 밟은 범위를 편마다 적었다. 명령이 못 도는 것을 고쳤다. - kubectl 을 `kc-lab-1` 에서 치라고 적었는데 그 기계에 kubeconfig 가 없다. 라벨 639개와 각 편의 「어디서 치는가」를 `[lab host]` 로 옮겼다 - `-o custom-columns=…[0]…` 이 zsh 에서 글로브로 읽혀 안 돈다. 28곳에 따옴표 - busybox `sed` 가 끝 개행을 안 붙여 A-3 의 측정이 언제나 0 이었다 - `--token-file ~/node-token` 뒤에 그 파일을 지우면 k3s agent 가 재부팅을 못 견딘다. `/etc/rancher/node-token` 으로 옮기는 처방을 재서 넣었다 - 게스트에 없는 도구를 전제로 한 명령 넷 — `conntrack`·`dig`·`strings`·`nginx -v` - `echo` 와 JWT 헤더가 `"이름" : [ 값 ]` 으로 찍는데 문서는 공백 없이 옮겨 적어 그 실측으로 만든 grep·sed 가 한 줄도 못 잡는다 - B-0 이 `directAccessGrantsEnabled` 와 계정 완성을 빠뜨려 B-3 이 못 돈다 - D-4·D-4a 가 `test-server` 와 `certbot-renew.*` 를 가리키는데 실제로는 `kc-lab-edge` 의 `certbot.service` 다 - `virsh setmaxmem --config` 를 `dominfo` 로 판정하면 틀린다. `--inactive` 로 - `LIBVIRT_DEFAULT_URI` 를 rc 에만 넣으면 `ssh host '명령'` 에서 안 먹는다 결과가 조건부인 것을 갈랐다. - readiness 는 즉시 안 뒤집힌다. A-1·A-2 의 60초 창을 적었다 - 03 의 층 ②③ `301` 은 04 이후의 값이고 그 단계에서는 `404` 다 - A-0 의 로그 필터를 요청 직후에 치면 정반대 결론이 나온다 - A-5 의 한 방향 차단은 잠깐 `1` 이었다 `2` 로 돌아온다 증거는 두 프로젝트의 `evidence/raw/` 에 99벌을 README 와 함께 남겼다. 비밀은 길이만 적었고 화면에 찍힌 토큰은 가렸다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ab59130196
commit
024362d096
+63
-41
@@ -41,9 +41,11 @@ PostgreSQL 을 정상 종료시키고 refresh 와 새 로그인과 관리 API
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 전부 `[kc-lab-1]` 에서 `kubectl` 과 `psql` 로 친다. 노드 자체를 건드리는 명령이 없어서 `kc-lab-2` 로 들어갈 일이 없다. `kubectl` 에 `sudo` 를 붙이지 않는다 — root 홈에는 `~/.kube/config` 가 없어 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝난다.
|
||||
명령은 전부 `[lab host]` 에서 `kubectl` 과 `psql` 로 친다. 노드 자체를 건드리는 명령이 없어서 게스트에 들어갈 일이 없다. `kubectl` 에 `sudo` 를 붙이지 않는다 — root 홈에는 `~/.kube/config` 가 없어 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝난다.
|
||||
|
||||
터미널은 둘을 연다. 하나는 탐침 파드 셸용이라 붙잡혀 있고, 하나는 관찰용이다. 그래서 `[kc-lab-1]` 라벨이 붙은 블록이 `[탐침 파드]` 블록 사이에 끼어 있으면 **관찰용 터미널에서 친다** — 파드 셸을 나가라는 뜻이 아니다. 나가라고 할 때는 `exit` 를 블록으로 따로 적는다.
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
터미널은 둘을 연다. 하나는 탐침 파드 셸용이라 붙잡혀 있고, 하나는 관찰용이다. 그래서 `[lab host]` 라벨이 붙은 블록이 `[탐침 파드]` 블록 사이에 끼어 있으면 **관찰용 터미널에서 친다** — 파드 셸을 나가라는 뜻이 아니다. 나가라고 할 때는 `exit` 를 블록으로 따로 적는다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -84,7 +86,7 @@ A-1 과의 대비가 이 실험의 값이다.
|
||||
|
||||
**이건 전면 장애를 만드는 실험이다.** 정문(`https://auth.hyeonworks.com`)이 실제로 `503` 이 된다. 이 실험대를 쓰는 다른 작업이 있으면 멈춘다. 정지 구간은 **1분 남짓**으로 짧게 잡는다. 되돌리는 명령은 아래 한 줄이다.
|
||||
|
||||
```bash label="[kc-lab-1] 중간에 그만둘 때 치는 한 줄"
|
||||
```bash label="[lab host] 중간에 그만둘 때 치는 한 줄"
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||||
```
|
||||
|
||||
@@ -98,7 +100,7 @@ kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||||
|
||||
**무엇을 보는가** — 세 파드의 상태와 배치.
|
||||
|
||||
```bash label="[kc-lab-1] 파드 배치를 본다"
|
||||
```bash label="[lab host] 파드 배치를 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
|
||||
@@ -116,7 +118,7 @@ postgres-7b474b88c8-sn9ff true 10.42.1.24 kc-lab-2
|
||||
|
||||
**무엇을 보는가** — 뒤의 모든 요청이 향할 주소.
|
||||
|
||||
```bash label="[kc-lab-1] 파드 주소를 변수에 담는다"
|
||||
```bash label="[lab host] 파드 주소를 변수에 담는다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
echo "$K0 $K1"
|
||||
@@ -128,14 +130,14 @@ echo "$K0 $K1"
|
||||
|
||||
**무엇을 보는가** — 두 노드가 아는 멤버 수.
|
||||
|
||||
```bash label="[kc-lab-1] ① 한 줄짜리 JSON 을 통째로 본다"
|
||||
```bash label="[lab host] ① 한 줄짜리 JSON 을 통째로 본다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
|
||||
```
|
||||
|
||||
라벨을 보고 나서 자른다. 아래 줄은 가이드가 미검증으로 표시했다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② 필요한 줄만 자른다"
|
||||
```bash label="[lab host] ② 필요한 줄만 자른다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
|
||||
| tr ',' '\n' | grep -E '"pod":|^"[0-9]'
|
||||
@@ -151,7 +153,7 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
|
||||
`--rm` 임시 파드는 매번 만들고 지우므로 느리고 경합이 있고, 토큰을 단계 사이로 넘길 수 없다. 이 실험은 DB 정지 전에 발급한 토큰을 정지 후에 써야 하므로 파드를 하나 띄워 두고 `exec` 로 단계를 이어간다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 상주 탐침 파드를 띄우고 Ready 까지 기다린다"
|
||||
```bash label="[lab host] ① 상주 탐침 파드를 띄우고 Ready 까지 기다린다"
|
||||
kubectl -n keycloak-lab run a2-probe --image=curlimages/curl:8.11.1 \
|
||||
--restart=Never \
|
||||
--env="K0=$K0" --env="K1=$K1" \
|
||||
@@ -165,20 +167,20 @@ kubectl -n keycloak-lab wait --for=condition=Ready pod/a2-probe --timeout=120s
|
||||
|
||||
비밀번호는 명령 치환으로 넘기므로 값이 터미널에도 셸 히스토리에도 남지 않는다. 존재와 길이만 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 비밀번호의 길이만 센다"
|
||||
```bash label="[lab host] ② 비밀번호의 길이만 센다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
|
||||
```
|
||||
|
||||
실측은 `19` 다(observed).
|
||||
|
||||
```bash label="[kc-lab-1] ③ 파드 안에도 값이 들어왔는지 길이로 본다"
|
||||
```bash label="[lab host] ③ 파드 안에도 값이 들어왔는지 길이로 본다"
|
||||
kubectl -n keycloak-lab exec a2-probe -- sh -c 'echo "K0=$K0 K1=$K1 PW길이=${#PW}"'
|
||||
```
|
||||
|
||||
`PW길이=0` 이면 `--env` 가 빈 값을 넘긴 것이므로 파드를 지우고 다시 띄운다. 이제부터는 파드 셸에 들어가 친다. 나올 때는 `exit` 이고 파드는 안 지워진다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 파드 셸로 들어간다"
|
||||
```bash label="[lab host] ④ 파드 셸로 들어간다"
|
||||
kubectl -n keycloak-lab exec -it a2-probe -- sh
|
||||
```
|
||||
|
||||
@@ -211,7 +213,7 @@ done
|
||||
|
||||
### 6. 세션이 각자 노드에만 캐시됐는가
|
||||
|
||||
```bash label="[kc-lab-1] 노드별 캐시 엔트리"
|
||||
```bash label="[lab host] 노드별 캐시 엔트리"
|
||||
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]'
|
||||
@@ -227,7 +229,7 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
|
||||
`keycloak-1` 이 `0` 인 것은 스크레이프 지연 때문이다. 방금 로그인했으므로 다음 15초 스크레이프에서 `1` 이 될 수 있고, 원래 실행 기록에도 「캐시 keycloak-0 = 1 건 / keycloak-1 = 0 건 (스크레이프 지연)」으로 적혀 있다. 판정에 쓰는 값은 양쪽이 다른지가 아니라 `keycloak-0` 이 확실히 가지고 있는지다.
|
||||
|
||||
```bash label="[kc-lab-1] DB 세션 수도 적어 둔다"
|
||||
```bash label="[lab host] DB 세션 수도 적어 둔다"
|
||||
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"
|
||||
```
|
||||
@@ -291,7 +293,7 @@ curl -s -o /dev/null -w 'well-known %{http_code}\n' \
|
||||
|
||||
밖에서 정문도 재 둔다.
|
||||
|
||||
```bash label="[kc-lab-1] 밖에서 본 정문"
|
||||
```bash label="[lab host] 밖에서 본 정문"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
|
||||
@@ -309,12 +311,12 @@ curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/mast
|
||||
| `delete pod` | Deployment 가 **곧바로 새로 만든다** — 몇 초 만에 돌아온다 |
|
||||
| 노드 정지 | Keycloak 도 같이 죽는다 — **두 장애가 섞인다** |
|
||||
|
||||
```bash label="[kc-lab-1] ① 시각을 남기고 replica 를 0 으로 내린다"
|
||||
```bash label="[lab host] ① 시각을 남기고 replica 를 0 으로 내린다"
|
||||
date '+%H:%M:%S 정지'
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=0
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드가 사라질 때까지 기다리고 시각을 남긴다"
|
||||
```bash label="[lab host] ② 파드가 사라질 때까지 기다리고 시각을 남긴다"
|
||||
kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s
|
||||
date '+%H:%M:%S 삭제완료'
|
||||
```
|
||||
@@ -339,7 +341,7 @@ pod/postgres-7b474b88c8-sn9ff condition met
|
||||
|
||||
### 1. postgres 파드가 하나도 없는가
|
||||
|
||||
```bash label="[kc-lab-1] 파드와 Deployment 를 함께 본다"
|
||||
```bash label="[lab host] 파드와 Deployment 를 함께 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
kubectl -n keycloak-lab get deploy postgres
|
||||
```
|
||||
@@ -348,7 +350,7 @@ kubectl -n keycloak-lab get deploy postgres
|
||||
|
||||
### 2. Keycloak 이 실제로 DB 를 못 잡고 있는가
|
||||
|
||||
```bash label="[kc-lab-1] 커넥션 풀 쪽 로그를 본다"
|
||||
```bash label="[lab host] 커넥션 풀 쪽 로그를 본다"
|
||||
kubectl -n keycloak-lab logs keycloak-0 --tail=40 | grep -A3 -i 'connection'
|
||||
```
|
||||
|
||||
@@ -366,19 +368,29 @@ Caused by: java.net.ConnectException: Connection refused
|
||||
|
||||
### 3. 파드가 죽지 않고 트래픽에서만 빠졌는가
|
||||
|
||||
```bash label="[kc-lab-1] Ready 와 재시작 횟수만 뽑아 본다"
|
||||
kubectl -n keycloak-lab get pods -o custom-columns=\
|
||||
NAME:.metadata.name,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount \
|
||||
```bash label="[lab host] Ready 와 재시작 횟수만 뽑아 본다"
|
||||
kubectl -n keycloak-lab get pods -o "custom-columns=NAME:.metadata.name,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount" \
|
||||
| grep keycloak
|
||||
```
|
||||
|
||||
실측은 `keycloak-0 false 0`, `keycloak-1 false 0` 이다(observed). `READY` 가 `false` 인데 `RESTARTS` 는 여전히 `0` 이다. 파드는 죽지 않았고 트래픽에서 빠졌을 뿐이다. `RESTARTS` 가 오르고 있으면 liveness 가 실패하는 것이고, 그 상태에서 무엇을 재든 「DB 없는 Keycloak」이 아니라 「재시작 중인 Keycloak」을 재게 된다. 이 `restarts=0` 이 자동 회복이라는 결론을 가능하게 하는 조건이다.
|
||||
실측은 `keycloak-0 false 0`, `keycloak-1 false 0` 이다(observed). 2026-09-17 에도 같은 두 줄을 받았다(observed).
|
||||
|
||||
**그 `false` 는 DB 를 내리자마자 나오지 않는다.** readiness 프로브가 `periodSeconds: 10` · `failureThreshold: 3` 이라 헬스가 먼저 실패하고 그로부터 최대 30초 뒤에 조건이 뒤집힌다. 2026-09-17 에 내린 직후에는 네 경로가 이미 전부 `500` 인데 파드는 아직 `true true` 였고 정문도 `200` 이었다. 약 50초 뒤에 `false false` · 정문 `503` 이 됐다(observed).
|
||||
|
||||
```text
|
||||
정지 직후 네 경로 500 500 500 500 · ready true,true · 정문 200
|
||||
약 50초 뒤 ready false,false · 엔드포인트 false,false · 정문 503
|
||||
```
|
||||
|
||||
**그 50초 사이의 화면을 보고 「DB 가 없어도 멀쩡하다」로 읽지 않는다.** 이미 500 인데 아직 트래픽에서 안 빠졌을 뿐이다. 이 편이 잡으려는 것도 그 어긋남이다 — 되는 것과 Ready 인 것은 다르다.
|
||||
|
||||
`READY` 가 `false` 인데 `RESTARTS` 는 여전히 `0` 이다. 파드는 죽지 않았고 트래픽에서 빠졌을 뿐이다. `RESTARTS` 가 오르고 있으면 liveness 가 실패하는 것이고, 그 상태에서 무엇을 재든 「DB 없는 Keycloak」이 아니라 「재시작 중인 Keycloak」을 재게 된다. 이 `restarts=0` 이 자동 회복이라는 결론을 가능하게 하는 조건이다.
|
||||
|
||||
## 관찰
|
||||
|
||||
탐침 파드 안에서 주입 전과 똑같은 명령을 다시 친다. 파드 셸에서 나갔다 들어오면 `RT0` `RT1` `AT` `CID` 가 사라지므로 셸을 붙잡고 있는 편이 낫다.
|
||||
|
||||
```bash label="[kc-lab-1] 셸을 놓쳤으면 다시 들어간다"
|
||||
```bash label="[lab host] 셸을 놓쳤으면 다시 들어간다"
|
||||
kubectl -n keycloak-lab exec -it a2-probe -- sh
|
||||
```
|
||||
|
||||
@@ -477,9 +489,9 @@ curl -s -o /dev/null -w '④ %{http_code}\n' --max-time 10 -H "Authorization: Be
|
||||
|
||||
같은 파드, 같은 포트인데 경로에 따라 `200` 과 `500` 이 갈린다. realm 공개키와 메타데이터는 메모리에 있으므로 DB 없이도 응답하고, 이론적으로는 이미 JWKS 를 캐시한 리소스 서버가 토큰 검증을 계속할 수 있다는 뜻이다. 이 실험대에는 독립 리소스 서버가 아직 없으므로 거기까지가 말할 수 있는 범위다.
|
||||
|
||||
```bash label="[kc-lab-1] Service 뒤에 누가 남았는지 본다"
|
||||
```bash label="[lab host] Service 뒤에 누가 남았는지 본다"
|
||||
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
|
||||
-o custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
|
||||
-o "custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready"
|
||||
```
|
||||
|
||||
실측은 이렇다(observed, `04-health-and-service.txt`).
|
||||
@@ -499,13 +511,13 @@ Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointS
|
||||
|
||||
사람이 눈으로 볼 때는 이쪽이 더 짧다.
|
||||
|
||||
```bash label="[kc-lab-1] 같은 것을 짧게 보는 형태"
|
||||
```bash label="[lab host] 같은 것을 짧게 보는 형태"
|
||||
kubectl -n keycloak-lab describe svc keycloak | grep -i endpoints
|
||||
```
|
||||
|
||||
밖에서 본다. 한 번 눈으로 볼 때는 헤더까지 본다.
|
||||
|
||||
```bash label="[kc-lab-1] 정문을 코드로 한 번, 헤더로 한 번"
|
||||
```bash label="[lab host] 정문을 코드로 한 번, 헤더로 한 번"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||||
curl -I https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
@@ -523,7 +535,7 @@ curl -I https://auth.hyeonworks.com/realms/master
|
||||
|
||||
헬스 본문이 까닭을 말한다. Keycloak 이미지에는 `curl` 이 없으므로 파드 밖에서 묻는다. 묻는 명령은 관찰용 터미널에서 치는데 `"http://$K0:9000/..."` 는 큰따옴표라 파드 안이 아니라 그 터미널의 셸이 편다. 두 값은 2번에서 잡았는데 그 터미널을 지금 탐침 파드 셸이 붙잡고 있으므로, 관찰용 터미널에서 두 줄을 다시 친다.
|
||||
|
||||
```bash label="[kc-lab-1] 관찰용 터미널에서도 파드 주소를 잡는다"
|
||||
```bash label="[lab host] 관찰용 터미널에서도 파드 주소를 잡는다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
echo "$K0 $K1"
|
||||
@@ -531,7 +543,7 @@ echo "$K0 $K1"
|
||||
|
||||
DB 가 없어도 이 두 줄은 된다. 파드 주소는 쿠버네티스 API 가 답하고, 주입 검증 3번에서 본 대로 Keycloak 파드는 재시작하지 않았으므로(`RESTARTS 0`) 값도 2번과 같다. 이 두 줄을 건너뛰고 다음 블록을 치면 `curl -s "http://:9000/health/ready"` 가 되어 URL 이 거부되고 빈 출력이 나온다. 전면 장애 한복판이라 그 빈 줄을 「헬스 엔드포인트까지 죽었다」로 읽기 쉬운데, 실제로는 헬스가 응답하고 네 항목 중 하나만 DOWN 이다.
|
||||
|
||||
```bash label="[kc-lab-1] 탐침 파드를 통해 헬스를 묻는다"
|
||||
```bash label="[lab host] 탐침 파드를 통해 헬스를 묻는다"
|
||||
kubectl -n keycloak-lab exec a2-probe -- \
|
||||
curl -s "http://$K0:9000/health/ready"
|
||||
```
|
||||
@@ -551,7 +563,7 @@ kubectl -n keycloak-lab exec a2-probe -- \
|
||||
|
||||
관측의 함정이 여기 있다.
|
||||
|
||||
```bash label="[kc-lab-1] up 지표를 본다"
|
||||
```bash label="[lab host] up 지표를 본다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=up' \
|
||||
| tr ',' '\n' | grep -E '"job":|"pod":|^"[0-9]'
|
||||
@@ -577,7 +589,7 @@ A-0 은 `up` 을 「가장 중요한 합성 지표」라고 썼는데 절반만
|
||||
|
||||
그럼 readiness 를 지표로 볼 수 있는지 물어본다.
|
||||
|
||||
```bash label="[kc-lab-1] 파드 readiness 지표가 있는지 묻는다"
|
||||
```bash label="[lab host] 파드 readiness 지표가 있는지 묻는다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=kube_pod_status_ready' \
|
||||
| head -c 300; echo
|
||||
@@ -597,12 +609,12 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
|
||||
**목적** — PostgreSQL 을 되살리고 Keycloak 이 스스로 돌아오는지 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 시각을 남기고 replica 를 1 로 올린다"
|
||||
```bash label="[lab host] ① 시각을 남기고 replica 를 1 로 올린다"
|
||||
date '+%H:%M:%S 재기동'
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 롤아웃이 끝날 때까지 기다린다"
|
||||
```bash label="[lab host] ② 롤아웃이 끝날 때까지 기다린다"
|
||||
kubectl -n keycloak-lab rollout status deployment/postgres --timeout=180s
|
||||
```
|
||||
|
||||
@@ -625,16 +637,14 @@ deployment "postgres" successfully rolled out
|
||||
|
||||
**목적** — 사람이 한 일이 DB 를 켠 것뿐인지 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① Ready 와 정문을 함께 친다"
|
||||
kubectl -n keycloak-lab get pods -o custom-columns=\
|
||||
NAME:.metadata.name,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount \
|
||||
```bash label="[lab host] ① Ready 와 정문을 함께 친다"
|
||||
kubectl -n keycloak-lab get pods -o "custom-columns=NAME:.metadata.name,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount" \
|
||||
| grep keycloak
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 재시작 횟수만 따로 센다"
|
||||
kubectl -n keycloak-lab get pods -o custom-columns=\
|
||||
NAME:.metadata.name,RESTARTS:.status.containerStatuses[0].restartCount | grep keycloak
|
||||
```bash label="[lab host] ② 재시작 횟수만 따로 센다"
|
||||
kubectl -n keycloak-lab get pods -o "custom-columns=NAME:.metadata.name,RESTARTS:.status.containerStatuses[0].restartCount" | grep keycloak
|
||||
```
|
||||
|
||||
**예상 결과** — 실측은 이렇다(observed).
|
||||
@@ -647,6 +657,18 @@ NAME:.metadata.name,RESTARTS:.status.containerStatuses[0].restartCount | grep ke
|
||||
|
||||
②의 실측은 `keycloak-0 0`, `keycloak-1 0` 이다(observed). 주입 검증에서 본 값 그대로다.
|
||||
|
||||
2026-09-17 에 다시 재 보니 `scale --replicas=1` 에서 두 파드가 `true` 로 돌아오기까지 **36초**였다(observed). 그 가운데 절반쯤은 PostgreSQL 파드가 뜨는 시간이다.
|
||||
|
||||
```text
|
||||
14:29:20 복구
|
||||
deployment "postgres" successfully rolled out
|
||||
14:29:56 ready=true,true 정문=200 재시작=0,0
|
||||
```
|
||||
|
||||
「약 15초」는 **DB 가 Ready 가 된 뒤부터** 센다. `scale` 을 친 순간부터 세면 더 걸리므로, 30초쯤 지나도 `503` 이면 그때 기다린다.
|
||||
|
||||
**④ 는 이 시점에 `401` 이 정상이다.** 2026-09-17 실행에서 ①②③ 이 `200` 인데 ④ 만 `401` 이었다(observed). access token 이 60초짜리라 정지 구간을 지나오며 만료됐을 뿐 DB 와는 무관하다. 위 7번의 시계 경고가 이것을 미리 말해 둔다.
|
||||
|
||||
**왜 필요한가** — 커넥션 풀이 스스로 재연결하고 readiness 가 다시 UP 이 되면서 Service 에 복귀했다. 회복 시간은 DB Ready 이후 약 15초, Keycloak 재시작은 불필요(`restarts=0`)였다. liveness 는 실패하면 재시작이라 재시작하면 나아지는 문제(교착, 메모리 누수)에 쓰고, readiness 는 실패하면 트래픽에서 격리라 재시작해도 안 나아지는 문제(의존 대상이 죽음)에 쓴다. DB 장애에 liveness 를 걸면 모든 파드가 무한 재시작하고, DB 가 돌아와도 CrashLoopBackOff 의 백오프 때문에 회복이 늦어지며, 재시작하면 캐시까지 날아간다.
|
||||
|
||||
**문제가 생기면** — 계속 `503` 이면 Keycloak 이 아직 재연결 중이다. 15~30초 더 기다리고 재시작하지 않는다.
|
||||
@@ -655,12 +677,12 @@ NAME:.metadata.name,RESTARTS:.status.containerStatuses[0].restartCount | grep ke
|
||||
|
||||
**목적** — 정상 종료가 데이터를 잃지 않았다는 것을 숫자로 확인하고 실험 도구를 치운다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 세션 수를 다시 센다"
|
||||
```bash label="[lab host] ① 세션 수를 다시 센다"
|
||||
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"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 탐침 파드를 직접 지운다"
|
||||
```bash label="[lab host] ② 탐침 파드를 직접 지운다"
|
||||
kubectl -n keycloak-lab delete pod a2-probe --ignore-not-found
|
||||
```
|
||||
|
||||
|
||||
+77
-39
@@ -41,7 +41,9 @@ PostgreSQL 을 진짜로 죽여 커밋됐다고 응답받은 로그인 중 몇
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 전부 `[kc-lab-1]` 에서 `kubectl` 과 `psql` 로 친다. 노드 자체를 건드리는 명령이 없어서 `kc-lab-2` 로 들어갈 일이 없다. `kubectl` 에 `sudo` 를 붙이지 않는다 — root 홈에는 `~/.kube/config` 가 없어 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝난다.
|
||||
명령은 전부 `[lab host]` 에서 `kubectl` 과 `psql` 로 친다. 노드 자체를 건드리는 명령이 없어서 게스트에 들어갈 일이 없다. `kubectl` 에 `sudo` 를 붙이지 않는다 — root 홈에는 `~/.kube/config` 가 없어 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝난다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
터미널은 둘이 반드시 필요하다. 터미널 ① 은 로그인 루프가 도는 동안 통째로 붙잡히고, 터미널 ② 에서 그 사이에 데이터베이스를 죽인다. 코드블록마다 어느 터미널인지 붙여 두었다.
|
||||
|
||||
@@ -95,7 +97,7 @@ A-0 을 끝내고 아직 아무것도 주입하기 전에 쓴 예측표에 이
|
||||
|
||||
지운 세션은 돌아오지 않는다. 되돌릴 수 있는 것은 문장 로깅 하나이고, 켜기 전에 끄는 명령을 먼저 읽어 둔다.
|
||||
|
||||
```bash label="[kc-lab-1] 중간에 그만둘 때 치는 한 묶음"
|
||||
```bash label="[lab host] 중간에 그만둘 때 치는 한 묶음"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "alter system reset log_statement" -c "select pg_reload_conf()"
|
||||
```
|
||||
@@ -108,7 +110,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**무엇을 보는가** — 세션 테이블의 컬럼 타입.
|
||||
|
||||
```bash label="[kc-lab-1] 세션 테이블의 정의를 본다"
|
||||
```bash label="[lab host] 세션 테이블의 정의를 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "\d offline_user_session"
|
||||
```
|
||||
@@ -137,7 +139,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**무엇을 보는가** — A-0 에서 잡은 것은 refresh 트랜잭션이었고 로그인(INSERT)도 그런지는 확인하지 않았다. 아니라면 로그인은 안 사라지고 이 측정 설계 자체가 성립하지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 문장 로깅을 잠깐 켠다"
|
||||
```bash label="[lab host] ① 문장 로깅을 잠깐 켠다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "alter system set log_statement='all'" -c "select pg_reload_conf()"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
@@ -146,7 +148,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
`none` 이면 `pg_reload_conf()` 가 안 돈 상태다. `alter system` 은 `postgresql.auto.conf` 에 쓸 뿐이고 reload 를 해야 적용된다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 탐침 파드를 띄우고 Ready 까지 기다린다"
|
||||
```bash label="[lab host] ② 탐침 파드를 띄우고 Ready 까지 기다린다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
kubectl -n keycloak-lab run a3-probe --image=curlimages/curl:8.11.1 \
|
||||
--restart=Never \
|
||||
@@ -159,13 +161,13 @@ kubectl -n keycloak-lab wait --for=condition=Ready pod/a3-probe --timeout=120s
|
||||
|
||||
명령줄에 비밀번호를 직접 쓰지 않는다. 원래 실험의 재현 절차에는 평문 비밀번호가 그대로 적혀 있는데 파드 안 `ps` 에도 셸 히스토리에도 남는다. `--env` 로 넘긴 값은 그 파드 안에서만 산다. 존재와 길이만 확인한다 — 실측은 `19` 다(observed).
|
||||
|
||||
```bash label="[kc-lab-1] ③ 비밀번호의 길이만 두 곳에서 센다"
|
||||
```bash label="[lab host] ③ 비밀번호의 길이만 두 곳에서 센다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
|
||||
kubectl -n keycloak-lab exec a3-probe -- sh -c 'echo "K0=$K0 PW길이=${#PW}"'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ④ 로그인 한 번을 보낸다"
|
||||
```bash label="[lab host] ④ 로그인 한 번을 보낸다"
|
||||
kubectl -n keycloak-lab exec a3-probe -- sh -c \
|
||||
'curl -s -o /dev/null -w "%{http_code}\n" -X POST \
|
||||
"http://$K0:8080/realms/master/protocol/openid-connect/token" \
|
||||
@@ -173,7 +175,7 @@ kubectl -n keycloak-lab exec a3-probe -- sh -c \
|
||||
-d username=admin -d "password=$PW"'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ⑤ 그 로그인의 트랜잭션을 로그에서 본다"
|
||||
```bash label="[lab host] ⑤ 그 로그인의 트랜잭션을 로그에서 본다"
|
||||
kubectl -n keycloak-lab logs deploy/postgres --since=60s \
|
||||
| grep -E 'BEGIN|insert into OFFLINE|synchronous_commit|COMMIT' | tail -20
|
||||
```
|
||||
@@ -196,7 +198,7 @@ kubectl -n keycloak-lab logs deploy/postgres --since=60s \
|
||||
|
||||
곧바로 끈다.
|
||||
|
||||
```bash label="[kc-lab-1] ⑥ 문장 로깅을 끄고 꺼졌는지 읽는다"
|
||||
```bash label="[lab host] ⑥ 문장 로깅을 끄고 꺼졌는지 읽는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "alter system reset log_statement" -c "select pg_reload_conf()"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
@@ -209,7 +211,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**무엇을 보는가** — 나중에 손실 창과 견줄 값.
|
||||
|
||||
```bash label="[kc-lab-1] 커밋과 WAL 관련 설정 네 줄"
|
||||
```bash label="[lab host] 커밋과 WAL 관련 설정 네 줄"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select name, setting, unit, source from pg_settings
|
||||
where name in ('commit_delay','synchronous_commit','wal_writer_delay','wal_writer_flush_after')"
|
||||
@@ -234,12 +236,12 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**무엇을 보는가** — 출발값.
|
||||
|
||||
```bash label="[kc-lab-1] ① 세션 행을 지운다"
|
||||
```bash label="[lab host] ① 세션 행을 지운다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "delete from offline_user_session"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 남은 행을 센다"
|
||||
```bash label="[lab host] ② 남은 행을 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select count(*) from offline_user_session where offline_flag='0'"
|
||||
```
|
||||
@@ -250,7 +252,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
### 5. 파드 셋의 재시작 횟수를 적어 둔다
|
||||
|
||||
```bash label="[kc-lab-1] 파드 셋의 상태"
|
||||
```bash label="[lab host] 파드 셋의 상태"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
|
||||
@@ -282,7 +284,7 @@ kubectl -n keycloak-lab get pods -o wide
|
||||
|
||||
0건이다. 파드 안에서 `( ... ) &` 로 띄운 루프가 `exec` 세션이 끝날 때 같이 죽었고 측정 자체가 없었다. 그래서 편집기로 파일을 연다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 편집기로 루프 파일을 연다"
|
||||
```bash label="[lab host] ① 편집기로 루프 파일을 연다"
|
||||
vim /tmp/a3-login-loop.sh
|
||||
```
|
||||
|
||||
@@ -299,23 +301,41 @@ while [ "$i" -lt 400 ]; do
|
||||
-d username=admin -d "password=$PW" \
|
||||
| sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
|
||||
if [ -n "$AT" ]; then
|
||||
echo "$AT" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null \
|
||||
| sed -n 's/.*"sid":"\([^"]*\)".*/\1/p' >> /tmp/sids
|
||||
SID=$(echo "$AT" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null \
|
||||
| sed -n 's/.*"sid":"\([^"]*\)".*/\1/p')
|
||||
[ -n "$SID" ] && echo "$SID" >> /tmp/sids
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo "루프 종료: $(wc -l < /tmp/sids) 건"
|
||||
```
|
||||
|
||||
**`sed` 의 출력을 파일로 바로 흘리지 않는다.** 변수에 받아 `echo` 로 다시 쓴다. 탐침 이미지의 셸이 busybox 이고 그 `sed` 는 **입력에 끝 개행이 없으면 출력에도 안 붙인다.** `base64 -d` 가 내놓는 JSON 에는 끝 개행이 없으므로, 바로 `>>` 로 흘리면 sid 가 줄바꿈 없이 이어 붙어 **파일 한 줄**이 된다. 2026-09-17 에 그대로 겪었다(observed) — 로그인 217건이 DB 에 들어갔는데 파일은 이랬다.
|
||||
|
||||
```text
|
||||
0 /tmp/sids
|
||||
9384 /tmp/sids
|
||||
```
|
||||
|
||||
`wc -l` 이 `0` 이고 `wc -c` 는 9384 다. **데이터는 다 있는데 줄이 하나도 없다.** 이 파일을 `wc -l`·`sort -u`·`comm` 으로 세는 것이 이 실험의 측정 전부이므로, 이 상태로는 손실 건수가 언제나 0 으로 나온다. `echo` 로 감싼 형태로 열 번 돌려 `10 줄 / 250 바이트` 를 받아 고쳤다(observed).
|
||||
|
||||
busybox 의 `sed` 가 그렇게 동작하는 것을 따로 확인했다(observed).
|
||||
|
||||
```text
|
||||
printf "%s" '{"sid":"ABC"}' | sed -n 's/.*"sid":"\([^"]*\)".*/\1/p' | od -c
|
||||
0000000 A B C
|
||||
0000003
|
||||
```
|
||||
|
||||
`/tmp/sids` 에는 클라이언트가 `200` 과 토큰을 실제로 받은 것만 쌓인다. `AT` 가 비면 아무것도 안 적으므로 이 파일이 「서버가 COMMIT 했다고 응답한 것」의 목록이고, 그게 이 실험의 시험군이다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 파일을 파드 안으로 밀어 넣고 줄 수를 센다"
|
||||
```bash label="[lab host] ② 파일을 파드 안으로 밀어 넣고 줄 수를 센다"
|
||||
kubectl -n keycloak-lab exec -i a3-probe -- sh -c 'cat > /tmp/a3-login-loop.sh' \
|
||||
< /tmp/a3-login-loop.sh
|
||||
kubectl -n keycloak-lab exec a3-probe -- wc -l /tmp/a3-login-loop.sh
|
||||
```
|
||||
|
||||
**예상 결과** — 줄 수가 `17 /tmp/a3-login-loop.sh` 로 나오면 들어갔다.
|
||||
**예상 결과** — 줄 수가 `19 /tmp/a3-login-loop.sh` 로 나오면 들어갔다. 원래 실행의 파일은 `17` 줄이었고, 위에서 sid 를 변수로 받는 두 줄로 바꾸면서 늘었다.
|
||||
|
||||
**왜 필요한가** — `kubectl cp` 도 되지만 컨테이너에 `tar` 가 있어야 한다. 이 실험대의 `curlimages/curl:8.11.1` 에 `tar` 가 있는지는 재지 않았다(unknown). 그래서 가이드는 `cat >` 로 밀어 넣는 쪽이 어디서나 통한다고 적고 그쪽을 골랐다.
|
||||
|
||||
@@ -455,7 +475,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||
|
||||
### 1. 시도 ① 의 검증 — 죽인 적이 없다
|
||||
|
||||
```bash label="[kc-lab-1] 새 파드를 기다리고 기동 로그를 본다"
|
||||
```bash label="[lab host] 새 파드를 기다리고 기동 로그를 본다"
|
||||
kubectl -n keycloak-lab rollout status deployment/postgres --timeout=180s
|
||||
kubectl -n keycloak-lab logs deploy/postgres | grep -E 'not properly shut down|redo|ready to accept'
|
||||
```
|
||||
@@ -471,7 +491,7 @@ kubectl -n keycloak-lab logs deploy/postgres | grep -E 'not properly shut down|r
|
||||
|
||||
그런데도 손실을 세어 보면 이렇게 나온다.
|
||||
|
||||
```bash label="[kc-lab-1] 클라이언트가 받은 건수와 DB 건수를 나란히 본다"
|
||||
```bash label="[lab host] 클라이언트가 받은 건수와 DB 건수를 나란히 본다"
|
||||
kubectl -n keycloak-lab exec a3-probe -- wc -l /tmp/sids
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select count(*) from offline_user_session where offline_flag='0'"
|
||||
@@ -492,7 +512,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
### 2. 시도 ② 의 검증 — 아무 일도 일어나지 않았다
|
||||
|
||||
```bash label="[kc-lab-1] 파드와 기동 로그의 마지막 세 줄"
|
||||
```bash label="[lab host] 파드와 기동 로그의 마지막 세 줄"
|
||||
kubectl -n keycloak-lab get pods -l app=postgres
|
||||
kubectl -n keycloak-lab logs deploy/postgres | grep -E 'not properly shut down|redo|ready to accept' | tail -3
|
||||
```
|
||||
@@ -522,7 +542,7 @@ kubectl -n keycloak-lab logs deploy/postgres | grep -E 'not properly shut down|r
|
||||
|
||||
### 3. 시도 ③ 의 검증 — 이번엔 걸렸다
|
||||
|
||||
```bash label="[kc-lab-1] 크래시 흔적 여섯 줄을 한 번에 본다"
|
||||
```bash label="[lab host] 크래시 흔적 여섯 줄을 한 번에 본다"
|
||||
kubectl -n keycloak-lab logs deploy/postgres --since=5m \
|
||||
| grep -E 'terminated by signal|reinitializing|not properly shut down|redo|checkpoint complete|ready to accept'
|
||||
```
|
||||
@@ -542,16 +562,15 @@ kubectl -n keycloak-lab logs deploy/postgres --since=5m \
|
||||
|
||||
여섯 줄이 순서대로 나온다. `terminated by signal 9` 는 내가 죽인 그 백엔드이고, `all server processes terminated; reinitializing` 은 postmaster 가 전체를 갈아엎기로 한 것이며, **`not properly shut down` 이 주입 성공 신호다 — 이게 없으면 결과를 해석하지 않는다.** `redo starts` 와 `redo done` 이 재생된 WAL 구간, `checkpoint complete` 가 재생 결과를 디스크에 고정한 것, 그리고 `ready to accept connections` 의 시각이 새로 찍혔다.
|
||||
|
||||
```bash label="[kc-lab-1] 파드 재시작 횟수를 센다"
|
||||
kubectl -n keycloak-lab get pods -l app=postgres -o custom-columns=\
|
||||
NAME:.metadata.name,RESTARTS:.status.containerStatuses[0].restartCount
|
||||
```bash label="[lab host] 파드 재시작 횟수를 센다"
|
||||
kubectl -n keycloak-lab get pods -l app=postgres -o "custom-columns=NAME:.metadata.name,RESTARTS:.status.containerStatuses[0].restartCount"
|
||||
```
|
||||
|
||||
`RESTARTS` 는 `0` 이다. 컨테이너의 PID 1 인 postmaster 는 살아 있고 자식만 갈아치웠다. 쿠버네티스 관점에서는 아무 일도 없었지만 데이터 관점에서는 전원이 나간 것과 같다.
|
||||
|
||||
## 관찰
|
||||
|
||||
```bash label="[kc-lab-1] ① 클라이언트가 받은 sid 목록을 꺼낸다"
|
||||
```bash label="[lab host] ① 클라이언트가 받은 sid 목록을 꺼낸다"
|
||||
kubectl -n keycloak-lab exec a3-probe -- cat /tmp/sids > /tmp/client-sids.txt
|
||||
wc -l /tmp/client-sids.txt
|
||||
head -3 /tmp/client-sids.txt
|
||||
@@ -567,12 +586,12 @@ hkcOv1QskUFmYveMLB6Hljra
|
||||
|
||||
DB 쪽은 먼저 총계를 보고 그다음 목록으로 뽑는다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 총계를 표로 본다"
|
||||
```bash label="[lab host] ② 총계를 표로 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select count(*) from offline_user_session where offline_flag='0'"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 값만 뽑아 파일로 내린다"
|
||||
```bash label="[lab host] ③ 값만 뽑아 파일로 내린다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select user_session_id from offline_user_session where offline_flag='0'" \
|
||||
> /tmp/db-sids.txt
|
||||
@@ -583,7 +602,7 @@ wc -l /tmp/db-sids.txt
|
||||
|
||||
차집합은 `comm` 으로 낸다. 정렬부터 한다 — 이 세 줄은 미검증이다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ④ 같은 정렬 순서로 맞춘 뒤 왼쪽 전용만 낸다"
|
||||
```bash label="[lab host] ④ 같은 정렬 순서로 맞춘 뒤 왼쪽 전용만 낸다"
|
||||
LC_ALL=C sort -u /tmp/client-sids.txt > /tmp/a.txt
|
||||
LC_ALL=C sort -u /tmp/db-sids.txt > /tmp/b.txt
|
||||
comm -23 /tmp/a.txt /tmp/b.txt
|
||||
@@ -608,13 +627,32 @@ comm -23 /tmp/a.txt /tmp/b.txt
|
||||
|
||||
로그인이 성공했다고 응답받았는데 세션이 존재하지 않는다. 153건 중 4건, 약 2.6% 다.
|
||||
|
||||
```bash label="[kc-lab-1] ⑤ 유실 건수를 센다"
|
||||
**2026-09-17 에 새 실험대에서 다시 재니 395건 중 3건이었다**(observed). 세 시도를 순서대로 밟았고 앞의 둘은 그때도 0건이었다.
|
||||
|
||||
```text
|
||||
시도 ① --grace-period=0 --force 유실 0건 — 새 파드 로그가 `database system was shut down at …`
|
||||
시도 ② kill -9 1 유실 0건 — RESTARTS 0, 파드 이름도 그대로
|
||||
시도 ③ 백엔드 PID 39 에 SIGKILL 유실 3건 — 클라이언트 395 · DB 392
|
||||
```
|
||||
|
||||
시도 ③ 의 로그가 남긴 것도 같았다.
|
||||
|
||||
```text
|
||||
server process (PID 39) was terminated by signal 9: Killed
|
||||
all server processes terminated; reinitializing
|
||||
database system was not properly shut down; automatic recovery in progress
|
||||
redo starts at 0/1F80A00
|
||||
```
|
||||
|
||||
**건수는 실행마다 다르다.** 크래시 순간에 아직 디스크로 못 내려간 트랜잭션만 사라지므로 로그인 속도와 `wal_writer_delay` 와 크래시 타이밍이 그 수를 정한다. 판정에 쓰는 것은 숫자가 아니라 **0 이 아니라는 것**이다.
|
||||
|
||||
```bash label="[lab host] ⑤ 유실 건수를 센다"
|
||||
comm -23 /tmp/a.txt /tmp/b.txt | wc -l
|
||||
```
|
||||
|
||||
사라지지 않은 것도 하나 본다. sid 는 첫 줄 `tail -1` 이 화면에 찍은 그 값을 옮겨 넣는다 — 로그인 루프가 만든 값이라 실행마다 다르다.
|
||||
|
||||
```bash label="[kc-lab-1] ⑥ 마지막 sid 가 DB 에 있는지 본다"
|
||||
```bash label="[lab host] ⑥ 마지막 sid 가 DB 에 있는지 본다"
|
||||
tail -1 /tmp/client-sids.txt
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select user_session_id, created_on, last_session_refresh
|
||||
@@ -676,12 +714,12 @@ ALTER DATABASE keycloak SET synchronous_commit = on;
|
||||
|
||||
**목적** — 다음 실험의 측정값이 로그 폭주 때문에 달라지지 않게 한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 두 설정을 읽는다"
|
||||
```bash label="[lab host] ① 두 설정을 읽는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "show log_statement" -c "show log_line_prefix"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② none 이 아니면 되돌리고 reload 한다"
|
||||
```bash label="[lab host] ② none 이 아니면 되돌리고 reload 한다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "alter system reset log_statement" -c "select pg_reload_conf()"
|
||||
```
|
||||
@@ -696,17 +734,17 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**목적** — DB 행과 캐시 엔트리를 함께 비우고 실험 도구를 치운다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 세션 행을 지운다"
|
||||
```bash label="[lab host] ① 세션 행을 지운다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "delete from offline_user_session"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드를 갈아 끼워 캐시를 비운다"
|
||||
```bash label="[lab host] ② 파드를 갈아 끼워 캐시를 비운다"
|
||||
kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 탐침 파드를 지운다"
|
||||
```bash label="[lab host] ③ 탐침 파드를 지운다"
|
||||
kubectl -n keycloak-lab delete pod a3-probe --ignore-not-found
|
||||
```
|
||||
|
||||
@@ -720,11 +758,11 @@ kubectl -n keycloak-lab delete pod a3-probe --ignore-not-found
|
||||
|
||||
**목적** — 크래시가 데이터 일부 손실인지 DB 파손인지 가른다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 파드 상태"
|
||||
```bash label="[lab host] ① 파드 상태"
|
||||
kubectl -n keycloak-lab get pods -l app=postgres
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 마이그레이션 이력 세 줄"
|
||||
```bash label="[lab host] ② 마이그레이션 이력 세 줄"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select id, dateexecuted from databasechangelog order by dateexecuted desc limit 3"
|
||||
```
|
||||
@@ -739,7 +777,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**목적** — 다음 실행이 옛 sid 목록을 읽지 않게 한다.
|
||||
|
||||
```bash label="[kc-lab-1] 이 실험이 만든 로컬 파일 다섯 개"
|
||||
```bash label="[lab host] 이 실험이 만든 로컬 파일 다섯 개"
|
||||
rm -f /tmp/client-sids.txt /tmp/db-sids.txt /tmp/a.txt /tmp/b.txt /tmp/a3-login-loop.sh
|
||||
```
|
||||
|
||||
|
||||
+26
-4
@@ -42,7 +42,7 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
| 터미널 | 어디서 치나 | 무엇을 치나 |
|
||||
|---|---|---|
|
||||
| A | `test-server` (VM 호스트) | `virsh` — 전원을 뽑고 다시 넣는다 |
|
||||
| B | `kc-lab-1` | `kubectl` — 관찰. 4b 에서는 이 터미널이 죽는다 |
|
||||
| B | `test-server` | `kubectl` — 관찰. 4b 에서는 이 터미널이 살아 있고 `kubectl` 만 막힌다 |
|
||||
| C | `test-server` | 밖에서 `curl`. 사용자 시점 |
|
||||
|
||||
4b 에서는 `kc-lab-2` 에도 붙는다. 터미널 A 와 같은 기계에서 `ssh kc-lab-2` 로 붙고, 이름이 안 풀리면 `ssh 192.168.122.12` 다.
|
||||
@@ -284,7 +284,9 @@ virsh destroy kc-lab-1
|
||||
observability prometheus-6774f94f7c-pzr2t
|
||||
```
|
||||
|
||||
④ 뒤에는 `차단 시각: 12:18:08` 과 `Domain 'kc-lab-1' destroyed` 가 나오고, 터미널 B 의 SSH 세션이 그대로 끊긴다.
|
||||
④ 뒤에는 `차단 시각: 12:18:08` 과 `Domain 'kc-lab-1' destroyed` 가 나오고, 원래 실행에서는 터미널 B 의 SSH 세션이 그대로 끊긴다 — 그 실행은 터미널 B 를 `kc-lab-1` 안에 두었기 때문이다.
|
||||
|
||||
**터미널 B 를 `test-server` 에 두면 세션은 안 끊긴다.** 대신 `kubectl` 이 API 서버를 못 찾는다 — k3s server 가 방금 내린 그 노드에 있고 kubeconfig 의 `server:` 가 `https://192.168.122.11:6443` 을 가리키기 때문이다. 화면에 남는 것이 「셸이 죽었다」에서 「`kubectl` 이 안 닿는다」로 바뀔 뿐, **관찰이 멈춘다는 사실은 같다.** 이 실험이 보여 주려는 것도 그것이다 — 죽은 노드로 관찰하고 있었으면 죽는 순간을 못 본다. 터미널 C 의 `curl` 이 그래서 따로 있다.
|
||||
|
||||
**왜 필요한가** — `traefik` 이 이 노드에 있고 `replicas` 가 `1` 이라, 이 노드를 뽑으면 클러스터로 들어갈 문이 사라진다. 인벤토리를 먼저 뽑아 두지 않으면 4b 에서 무엇이 없어졌는지 사후에 못 센다.
|
||||
|
||||
@@ -345,6 +347,16 @@ done
|
||||
+60초 node=NotReady | keycloak-0=Running postgres-7b474b88c8-2gf27=Running | 외부 HTTP 503
|
||||
```
|
||||
|
||||
2026-09-17 에 새 실험대에서 다시 재니 같은 모양이었다(observed). 차단은 `14:39:28` 이다.
|
||||
|
||||
```text
|
||||
14:39:56 (+28초) kc-lab-2=Ready | keycloak-0=Running postgres=Running | 정문 000
|
||||
14:40:34 (+66초) kc-lab-2=NotReady | keycloak-0=Running postgres=Running | 정문 503
|
||||
14:42:41 (+3분) kc-lab-2=NotReady | keycloak-0=Running postgres=Running | 정문 503
|
||||
```
|
||||
|
||||
**넘어가는 대목은 같고 초는 실행마다 다르다.** 30초 간격으로 쟀으므로 이번 판에서는 `+28초` 와 `+66초` 사이 어디쯤이다. `node-monitor-grace-period` 가 40초라 그 범위에 들어간다. 그 뒤로 3분이 지나도 두 파드는 계속 `Running` 이었다.
|
||||
|
||||
**이 결과가 뜻하는 것** — `+30초` 줄과 `+45초` 줄 사이에서 노드 상태가 넘어간다. kube-controller-manager 는 kubelet 의 하트비트가 `node-monitor-grace-period`(이 실험대에서 40초) 동안 없어야 `NotReady` 로 바꾸고, 그 40초 동안 쿠버네티스는 거짓말을 한다. 사용자는 그 40초에도 이미 장애를 겪고 있고 `000` 이 그것을 말한다. 여기서 주입이 안 걸렸다고 결론 내리면 틀린다 — 기계는 꺼져 있고 쿠버네티스가 아직 모를 뿐이다.
|
||||
|
||||
처음 40초가 `503` 이 아니라 `000` 인 까닭은 층이 다르기 때문이다. `000` 은 `curl` 이 응답 자체를 못 받은 것(타임아웃 또는 연결 실패)이고, `503` 은 nginx 와 Traefik 은 살아 있고 뒤로 보낼 파드가 없는 것이다. 엣지 nginx(`kc-lab-edge`)의 upstream 에는 두 노드가 다 들어 있다.
|
||||
@@ -379,8 +391,7 @@ kubectl get nodes
|
||||
### 1. 죽은 파드가 산 파드보다 건강해 보인다
|
||||
|
||||
```bash label="[터미널 B] ① 파드마다 phase 와 ready 와 노드를 함께"
|
||||
kubectl -n keycloak-lab get pods -o custom-columns=\
|
||||
NAME:.metadata.name,PHASE:.status.phase,READY:.status.containerStatuses[0].ready,NODE:.spec.nodeName
|
||||
kubectl -n keycloak-lab get pods -o "custom-columns=NAME:.metadata.name,PHASE:.status.phase,READY:.status.containerStatuses[0].ready,NODE:.spec.nodeName"
|
||||
```
|
||||
|
||||
```text
|
||||
@@ -482,6 +493,17 @@ watch -n 30 'kubectl -n keycloak-lab get pods -o wide'
|
||||
|
||||
`+240초` 와 `+270초` 사이에 두 가지가 동시에 일어난다. `kc-lab-2` 의 파드들이 `Terminating` 으로 바뀌고, 새 이름의 postgres 파드가 생기며 `Pending` 이다. 축출이 시작됐는데 `Terminating` 이 안 끝나고 새 파드는 뜨지 못하며, 두 문제는 원인이 다르다.
|
||||
|
||||
2026-09-17 에 새 실험대에서 다시 재니 같은 모양이었다(observed). 차단 `05:39:28` 에 대해 두 파드의 `deletionTimestamp` 가 `05:44:17` 로 찍혔다 — **4분 49초**다.
|
||||
|
||||
```text
|
||||
keycloak-0 Terminating kc-lab-2 deletionTimestamp=2026-09-17T05:44:17Z
|
||||
postgres-7b474b88c8-prlxx Terminating kc-lab-2 deletionTimestamp=2026-09-17T05:44:17Z
|
||||
postgres-7b474b88c8-5zp6v Pending <none> deletionTimestamp=<none>
|
||||
keycloak-1 Running kc-lab-1 deletionTimestamp=<none>
|
||||
```
|
||||
|
||||
**StatefulSet 과 Deployment 의 처지가 그대로 갈린다.** 새 이름으로 생긴 것은 postgres 쪽뿐이고 `keycloak-0` 은 대체 파드 없이 `Terminating` 으로만 남는다. StatefulSet 은 이름이 고정이라 같은 이름의 파드를 둘로 만들 수 없고, 죽은 노드의 kubelet 이 「지웠다」고 답해 주지 못하므로 그 이름이 영원히 안 비워진다.
|
||||
|
||||
### 6. 새 파드가 갈 곳이 없다
|
||||
|
||||
```bash label="[터미널 B] ① Pending 인 파드 이름을 먼저 확인한다"
|
||||
|
||||
+107
-68
@@ -41,9 +41,13 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
`kubectl` 은 `kc-lab-1` 에서 친다. `iptables` 는 노드 자체를 건드리는 명령이라 게스트 셸이 필요하고, 두 노드에 각각 넣어야 하며 어느 노드에 넣느냐가 결과를 가른다. 이쪽 노드의 규칙은 `[kc-lab-1]` 에서 그대로 치고, 반대 노드의 규칙은 `ssh kc-lab-2` 로 붙어서 친다. 코드블록마다 어느 셸인지 붙여 두었다.
|
||||
`kubectl` 은 `[lab host]` 에서 친다. `iptables` 는 노드 자체를 건드리는 명령이라 게스트 셸이 필요하고, 두 노드에 각각 넣어야 하며 어느 노드에 넣느냐가 결과를 가른다. 이쪽 노드의 규칙은 `[kc-lab-1]` 에서 그대로 치고, 반대 노드의 규칙은 `ssh kc-lab-2` 로 붙어서 친다. 코드블록마다 어느 셸인지 붙여 두었다.
|
||||
|
||||
가이드는 터미널 둘을 권한다 — 하나는 상주 탐침 파드용, 하나는 관찰용이다. 다만 이 절차에는 탐침 파드의 셸 안에서 치는 명령이 하나도 없다. `a5-probe` 는 `kubectl exec` 으로만 쓰므로 명령은 전부 `[kc-lab-1]` 에서 치고, `$K0` 와 `$K1` 도 그 셸의 변수다. 다른 터미널에서 관찰 §7 의 헬스체크를 치면 두 변수가 빈 문자열이라 양쪽 다 안 닿고, 그 화면을 「둘 다 DOWN」으로 읽게 된다. 실제로는 한쪽만 `DOWN` 이다.
|
||||
**`[kc-lab-1]` 라벨이 붙은 블록은 게스트 셸이다.** lab host 에서 `ssh kc-lab-1` 로 들어가서 치고, 끝나면 `exit` 로 나온다. `ssh kc-lab-2 '…'` 한 줄 형태는 lab host 에서 그대로 쳐도 되므로 `[lab host]` 로 두었다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
가이드는 터미널 둘을 권한다 — 하나는 상주 탐침 파드용, 하나는 관찰용이다. 다만 이 절차에는 탐침 파드의 셸 안에서 치는 명령이 하나도 없다. `a5-probe` 는 `kubectl exec` 으로만 쓰므로 명령은 전부 `[lab host]` 에서 치고, `$K0` 와 `$K1` 도 그 셸의 변수다. 다른 터미널에서 관찰 §7 의 헬스체크를 치면 두 변수가 빈 문자열이라 양쪽 다 안 닿고, 그 화면을 「둘 다 DOWN」으로 읽게 된다. 실제로는 한쪽만 `DOWN` 이다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -101,18 +105,18 @@ ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING ; sudo iptables -F FORWARD'
|
||||
|
||||
이쪽 노드의 규칙을 먼저 걷어낸다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 지우기 전에 무엇이 있었는지 본다"
|
||||
sudo iptables -S FORWARD
|
||||
```bash label="[lab host] ① 지우기 전에 무엇이 있었는지 본다"
|
||||
ssh kc-lab-1 'sudo iptables -S FORWARD'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 두 체인을 비운다"
|
||||
sudo iptables -t raw -F PREROUTING
|
||||
sudo iptables -F FORWARD
|
||||
```bash label="[lab host] ② kc-lab-1 의 두 체인을 비운다"
|
||||
ssh kc-lab-1 'sudo iptables -t raw -F PREROUTING'
|
||||
ssh kc-lab-1 'sudo iptables -F FORWARD'
|
||||
```
|
||||
|
||||
그다음 반대 노드에 붙는다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 게스트 셸로 들어간다"
|
||||
```bash label="[lab host] ③ 게스트 셸로 들어간다"
|
||||
ssh kc-lab-2
|
||||
```
|
||||
|
||||
@@ -142,11 +146,11 @@ exit
|
||||
|
||||
**무엇을 확인하는가** — 어느 파드가 어느 노드에 있고 IP(Internet Protocol 주소)가 무엇인지.
|
||||
|
||||
```bash label="[kc-lab-1] ① 배치와 IP"
|
||||
```bash label="[lab host] ① 배치와 IP"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 뒤에서 계속 쓸 두 값을 셸 변수에 담는다"
|
||||
```bash label="[lab host] ② 뒤에서 계속 쓸 두 값을 셸 변수에 담는다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
echo "K0=$K0 K1=$K1"
|
||||
@@ -167,12 +171,12 @@ postgres-7b474b88c8-9cmsv 1/1 Running 0 14m
|
||||
|
||||
**무엇을 확인하는가** — 지금 코디네이터가 하나인지, 그리고 뷰 ID 가 몇인지.
|
||||
|
||||
```bash label="[kc-lab-1] ① 디스커버리는 DB 가 말한다"
|
||||
```bash label="[lab host] ① 디스커버리는 DB 가 말한다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select name, ip, coord from jgroups_ping order by name"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 뷰는 로그가 말한다"
|
||||
```bash label="[lab host] ② 뷰는 로그가 말한다"
|
||||
kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
|
||||
kubectl -n keycloak-lab logs keycloak-1 | grep ISPN000094 | tail -1
|
||||
```
|
||||
@@ -199,12 +203,12 @@ ISPN000094: [keycloak-0-24309(v=16.0.12)|13] (2) [keycloak-0-24309(v=16.0.12), k
|
||||
|
||||
Keycloak 컨테이너에는 `curl` 도 `wget` 도 없다(`exit 127`).
|
||||
|
||||
```bash label="[kc-lab-1] ① 한 줄짜리 JSON 을 처음 한 번은 그대로 본다"
|
||||
```bash label="[lab host] ① 한 줄짜리 JSON 을 처음 한 번은 그대로 본다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 미검증 — 라벨과 값만 남겨 자르는 형태"
|
||||
```bash label="[lab host] ② 미검증 — 라벨과 값만 남겨 자르는 형태"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
|
||||
| tr ',' '\n' | grep -E '"pod":|^"[0-9]'
|
||||
@@ -240,9 +244,15 @@ sudo conntrack -L 2>/dev/null | grep 7800
|
||||
ssh kc-lab-2 'sudo conntrack -L 2>/dev/null | grep 7800'
|
||||
```
|
||||
|
||||
:::warning
|
||||
|
||||
**게스트에 `conntrack` 이 깔려 있지 않다.** cloud-init 이 까는 것은 `curl` 과 `nftables` 뿐이라 이 명령은 `sudo: conntrack: command not found` 로 끝나는데, `2>/dev/null` 이 그 한 줄을 지우고 종료 코드도 `0` 이다. 빈 화면이 「7800 연결이 없다」로 읽힌다. 두 노드에 먼저 `sudo apt install -y conntrack` 을 치거나, 패키지 없이 `sudo grep 7800 /proc/net/nf_conntrack` 으로 커널 표를 직접 읽는다. A-1 에서 재서 확인했다(observed).
|
||||
|
||||
:::
|
||||
|
||||
**따라 하는 사람은** 둘째 줄을 나눈다. 붙고 나서 원격 셸에서 친다. 이 나눈 형태는 이 실험대에서 치지 않았다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 반대 노드에 붙는다"
|
||||
```bash label="[lab host] ① 반대 노드에 붙는다"
|
||||
ssh kc-lab-2
|
||||
```
|
||||
|
||||
@@ -265,11 +275,11 @@ ESTABLISHED src=10.42.1.77 dst=10.42.0.42 sport=60485 dport=7800
|
||||
|
||||
**무엇을 확인하는가** — 정문이 지금 무엇을 답하는지.
|
||||
|
||||
```bash label="[kc-lab-1] ① 응답을 통째로 읽는 형태"
|
||||
```bash label="[lab host] ① 응답을 통째로 읽는 형태"
|
||||
curl -I --max-time 8 https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 여러 번 비교할 것이므로 코드만 뽑는 형태"
|
||||
```bash label="[lab host] ② 여러 번 비교할 것이므로 코드만 뽑는 형태"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
|
||||
@@ -297,32 +307,32 @@ curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.co
|
||||
|
||||
**목적** — `FORWARD` 최상단에 넣으면 conntrack 승인보다 먼저 평가되리라는 가설을 실제로 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 7800 을 FORWARD 1번에 넣는다"
|
||||
```bash label="[lab host] ① 7800 을 FORWARD 1번에 넣는다"
|
||||
ssh kc-lab-2 "sudo iptables -I FORWARD 1 -p tcp -d $K0 --dport 7800 -j DROP"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 57800 도 같이 막는다"
|
||||
```bash label="[lab host] ② 57800 도 같이 막는다"
|
||||
ssh kc-lab-2 "sudo iptables -I FORWARD 1 -p tcp -d $K0 --dport 57800 -j DROP"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 주입 시각"
|
||||
```bash label="[lab host] ③ 주입 시각"
|
||||
date '+%H:%M:%S 주입'
|
||||
```
|
||||
|
||||
**예상 결과** — `iptables` 는 아무것도 찍지 않는다. `주입: 12:28:23` 만 남는다.
|
||||
|
||||
이 줄들은 「전제와 되돌리기」의 중단 절차처럼 나눠 치면 안 된다. 거기는 `ssh kc-lab-2` 로 붙고 원격 셸에서 쳤지만, 여기는 `$K0` 가 들어간다. `$K0` 는 `[kc-lab-1]` 셸의 변수라 원격 셸에는 없고, 나눠 치면 빈 문자열이 들어가 `-d` 없는 규칙이 걸린다. 큰따옴표가 그 값을 `[kc-lab-1]` 에서 펴서 보내므로 한 줄 형태 그대로 친다.
|
||||
이 줄들은 「전제와 되돌리기」의 중단 절차처럼 나눠 치면 안 된다. 거기는 `ssh kc-lab-2` 로 붙고 원격 셸에서 쳤지만, 여기는 `$K0` 가 들어간다. `$K0` 는 `[lab host]` 셸의 변수라 원격 셸에는 없고, 나눠 치면 빈 문자열이 들어가 `-d` 없는 규칙이 걸린다. 큰따옴표가 그 값을 `[lab host]` 에서 펴서 보내므로 한 줄 형태 그대로 친다.
|
||||
|
||||
**왜 필요한가** — 57800 을 같이 막는 것은 FD_SOCK2(장애 감지 채널)가 `bind_port + 50000` 을 쓰기 때문이다. 7800 만 막으면 장애 감지는 계속 통해서 분단이 어정쩡해진다.
|
||||
|
||||
**결과를 본 다음 반드시 걷어낸다.** 주입 검증 §1 을 치고 여기로 와서 ④ 와 ⑤ 를 친다. 시도 ② 는 같은 두 포트를 같은 노드에서 다시 막으므로, `FORWARD` 에 남은 `DROP` 둘을 그대로 두면 어느 테이블이 패킷을 잡았는지 가릴 수 없다. 치우는 명령은 넣을 때와 인자가 같아야 한다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 넣을 때와 같은 인자로 지운다"
|
||||
```bash label="[lab host] ④ 넣을 때와 같은 인자로 지운다"
|
||||
ssh kc-lab-2 "sudo iptables -D FORWARD -p tcp -d $K0 --dport 7800 -j DROP"
|
||||
ssh kc-lab-2 "sudo iptables -D FORWARD -p tcp -d $K0 --dport 57800 -j DROP"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ⑤ 지워졌는지 줄 번호로 확인한다"
|
||||
```bash label="[lab host] ⑤ 지워졌는지 줄 번호로 확인한다"
|
||||
ssh kc-lab-2 'sudo iptables -L FORWARD -n --line-numbers | head -5'
|
||||
```
|
||||
|
||||
@@ -353,15 +363,15 @@ netfilter 의 처리 순서가 그 근거다.
|
||||
| filter FORWARD 직접 | 순서에 따라 | 경쟁한다 (kube-router 가 밀어낸다) |
|
||||
| raw PREROUTING | 끊는다 | 없다 — CNI 가 안 쓰는 테이블 |
|
||||
|
||||
```bash label="[kc-lab-1] ① 테이블만 바꾸고 노드와 목적지는 그대로 둔다"
|
||||
```bash label="[lab host] ① 테이블만 바꾸고 노드와 목적지는 그대로 둔다"
|
||||
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 7800 -j DROP"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 57800 도 같이"
|
||||
```bash label="[lab host] ② 57800 도 같이"
|
||||
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 57800 -j DROP"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 주입 시각"
|
||||
```bash label="[lab host] ③ 주입 시각"
|
||||
date '+%H:%M:%S 주입'
|
||||
```
|
||||
|
||||
@@ -371,11 +381,11 @@ date '+%H:%M:%S 주입'
|
||||
|
||||
**이 규칙도 반드시 걷어낸다.** 주입 검증 §2 를 치고 여기로 와서 ④ 와 ⑤ 를 친다. **네 번째 주입이 `kc-lab-2` 의 `raw PREROUTING` 에 글자까지 같은 두 줄을 다시 넣는다.** 걷어내지 않으면 같은 규칙이 두 벌 쌓여 카운터가 네 줄로 갈리고, 검증 §4 의 예상 결과는 그 모양을 적어 두지 않았다. 지우기 전에 `-L` 로 무엇이 있는지 본다. `-F` 는 체인 전체를 비운다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 무엇이 있는지 먼저 본다"
|
||||
```bash label="[lab host] ④ 무엇이 있는지 먼저 본다"
|
||||
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n --line-numbers'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ⑤ 체인을 비운다"
|
||||
```bash label="[lab host] ⑤ 체인을 비운다"
|
||||
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
|
||||
```
|
||||
|
||||
@@ -385,15 +395,23 @@ ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
|
||||
|
||||
**목적** — 7800 으로 실제로 들어가는 패킷을 잡는다. 목적지 파드가 있는 노드에서 잡아야 한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① keycloak-1 의 IP 를 목적지로, 이 노드에 넣는다"
|
||||
sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K1 --dport 7800 -j DROP
|
||||
```bash label="[lab host] ① keycloak-1 의 IP 를 목적지로, 그 파드가 있는 노드에 넣는다"
|
||||
ssh kc-lab-1 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K1 --dport 7800 -j DROP"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 57800 도 같이"
|
||||
sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K1 --dport 57800 -j DROP
|
||||
```bash label="[lab host] ② 57800 도 같이"
|
||||
ssh kc-lab-1 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K1 --dport 57800 -j DROP"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 주입 시각"
|
||||
**`ssh kc-lab-1 "…"` 한 줄 형태인 까닭이 반대 노드 때와 같다.** `$K1` 은 `[lab host]` 셸의 변수다. `ssh kc-lab-1` 로 먼저 들어가서 치면 그 셸에는 그 변수가 없어 **빈 문자열**이 들어가고, `-d` 없는 규칙이 걸려 **7800 으로 가는 모든 패킷**이 끊긴다. 2026-09-17 에 게스트 셸에서 확인했다(observed).
|
||||
|
||||
```text
|
||||
게스트 셸의 K1=[]
|
||||
```
|
||||
|
||||
큰따옴표가 값을 `[lab host]` 에서 펴서 보내므로 이 두 줄은 한 줄 형태 그대로 친다.
|
||||
|
||||
```bash label="[lab host] ③ 주입 시각"
|
||||
date '+%H:%M:%S 주입'
|
||||
```
|
||||
|
||||
@@ -417,15 +435,15 @@ date '+%H:%M:%S 주입'
|
||||
|
||||
**목적** — `kc-lab-1` 의 규칙은 그대로 두고 `kc-lab-2` 에 반대 방향을 더해 실제 분단을 만든다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 반대 방향을 반대 노드에 더한다"
|
||||
```bash label="[lab host] ① 반대 방향을 반대 노드에 더한다"
|
||||
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 7800 -j DROP"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 57800 도 같이"
|
||||
```bash label="[lab host] ② 57800 도 같이"
|
||||
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 57800 -j DROP"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 주입 시각"
|
||||
```bash label="[lab host] ③ 주입 시각"
|
||||
date '+%H:%M:%S 주입'
|
||||
```
|
||||
|
||||
@@ -438,6 +456,27 @@ date '+%H:%M:%S 주입'
|
||||
|
||||
**왜 필요한가** — 한 방향만 막으면 JGroups 가 열린 방향으로 다시 붙는다. 양쪽을 막아야 `coord = t` 가 둘이 된다.
|
||||
|
||||
2026-09-17 에 그 「다시 붙는」 과정을 초 단위로 봤다(observed). 한 방향만 막은 채로 두면 **잠깐 `1` 로 떨어졌다가 2분 안에 `2` 로 돌아온다.**
|
||||
|
||||
```text
|
||||
주입 +60초 keycloak-1=1 keycloak-0=1 ← 여기서 멈추면 「분단됐다」로 읽는다
|
||||
주입 +2분 keycloak-1=2 keycloak-0=2
|
||||
주입 +3분 keycloak-1=2 keycloak-0=2
|
||||
```
|
||||
|
||||
**그 첫 값을 결론으로 삼지 않는다.** 한 방향 차단은 한 번 재서 판정하지 않는다. 2~3분 두고 값이 돌아오는지를 본다.
|
||||
|
||||
양방향으로 막은 뒤의 실측은 이렇다(observed).
|
||||
|
||||
```text
|
||||
kc-lab-1 의 raw PREROUTING 7800: 21건 57800: 21건
|
||||
kc-lab-2 의 raw PREROUTING 7800: 19건 57800: 0건
|
||||
멤버 수 keycloak-1=1 keycloak-0=1
|
||||
jgroups_ping keycloak-0-13476 t · keycloak-1-8002 t
|
||||
```
|
||||
|
||||
분단을 가장 짧게 증명하는 것은 `coord` 가 둘 다 `t` 로 찍히는 순간이다. **`kc-lab-2` 쪽 57800 카운터만 `0` 인 것도 정상이다** — FD_SOCK2 가 그 방향에서 안 쓰였을 뿐이고, 7800 쪽이 올라갔으면 규칙은 걸렸다. 두 체인을 비우자 2분 안에 `2` 와 `coord` 하나로 돌아왔다(observed).
|
||||
|
||||
**문제가 생기면** — 양쪽 카운터를 다 본다. 한쪽만 걸리면 그것은 여전히 단방향이다.
|
||||
|
||||
## 주입 검증
|
||||
@@ -446,7 +485,7 @@ date '+%H:%M:%S 주입'
|
||||
|
||||
### 1. 시도 ① — 넣은 직후에는 맞게 보인다
|
||||
|
||||
```bash label="[kc-lab-1] ① 규칙과 카운터를 함께 본다"
|
||||
```bash label="[lab host] ① 규칙과 카운터를 함께 본다"
|
||||
ssh kc-lab-2 'sudo iptables -L FORWARD -n -v --line-numbers'
|
||||
```
|
||||
|
||||
@@ -482,7 +521,7 @@ kube-router 가 주기적으로 자기 체인을 `FORWARD` 최상단에 다시
|
||||
|
||||
### 2. 시도 ② — CNI 와 경쟁하지도 않는데 0 이다
|
||||
|
||||
```bash label="[kc-lab-1] ① raw 체인의 카운터"
|
||||
```bash label="[lab host] ① raw 체인의 카운터"
|
||||
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n -v'
|
||||
```
|
||||
|
||||
@@ -498,8 +537,8 @@ ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n -v'
|
||||
|
||||
### 3. 성공한 주입 — 처음으로 숫자가 올라간다
|
||||
|
||||
```bash label="[kc-lab-1] ① 이 노드의 raw 체인"
|
||||
sudo iptables -t raw -L PREROUTING -n -v
|
||||
```bash label="[lab host] ① 그 노드의 raw 체인"
|
||||
ssh kc-lab-1 'sudo iptables -t raw -L PREROUTING -n -v'
|
||||
```
|
||||
|
||||
```text
|
||||
@@ -524,11 +563,11 @@ sudo iptables -t raw -L PREROUTING -n -v
|
||||
|
||||
### 4. 양방향 주입 — 양쪽 카운터를 다 본다
|
||||
|
||||
```bash label="[kc-lab-1] ① 이 노드"
|
||||
sudo iptables -t raw -L PREROUTING -n -v
|
||||
```bash label="[lab host] ① kc-lab-1"
|
||||
ssh kc-lab-1 'sudo iptables -t raw -L PREROUTING -n -v'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 반대 노드"
|
||||
```bash label="[lab host] ② 반대 노드"
|
||||
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n -v'
|
||||
```
|
||||
|
||||
@@ -540,11 +579,11 @@ ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n -v'
|
||||
|
||||
**시도 ① 의 규칙이 `FORWARD` 에 걸려 있는 동안 친다.** 주입 §1 의 ④⑤ 로 걷어낸 뒤에 치면 규칙이 없는 상태를 재게 되는데, 화면은 규칙이 있든 없든 `외부 200` 이라 틀렸다는 신호가 안 나온다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 25초 간격으로 몇 번 친다"
|
||||
```bash label="[lab host] ① 25초 간격으로 몇 번 친다"
|
||||
kubectl -n keycloak-lab get pods | grep keycloak
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 같은 간격으로 밖에서"
|
||||
```bash label="[lab host] ② 같은 간격으로 밖에서"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
|
||||
@@ -573,7 +612,7 @@ curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.co
|
||||
|
||||
### 3. 로그 시각은 UTC 다
|
||||
|
||||
```bash label="[kc-lab-1] ① 최근 20분의 뷰 변화"
|
||||
```bash label="[lab host] ① 최근 20분의 뷰 변화"
|
||||
kubectl -n keycloak-lab logs keycloak-0 --since=20m | grep ISPN000094
|
||||
kubectl -n keycloak-lab logs keycloak-1 --since=20m | grep ISPN000094
|
||||
```
|
||||
@@ -604,8 +643,8 @@ Keycloak 컨테이너는 UTC(Coordinated Universal Time, 협정 세계시)로
|
||||
|
||||
앞에서 친 것과 똑같은 명령을 다시 친다. 그것이 대조하는 방법이다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 차단 전에 친 것과 같은 명령"
|
||||
sudo conntrack -L 2>/dev/null | grep 7800
|
||||
```bash label="[lab host] ① 차단 전에 친 것과 같은 명령"
|
||||
ssh kc-lab-1 'sudo conntrack -L 2>/dev/null | grep 7800'
|
||||
```
|
||||
|
||||
```text
|
||||
@@ -622,12 +661,12 @@ sudo conntrack -L 2>/dev/null | grep 7800
|
||||
|
||||
JGroups 는 막힌 연결이 죽자 반대 방향으로 새로 연결했다. FD_SOCK2 가 상대를 의심하기 전에 복구가 끝났고, 의심 카운터가 그것을 뒷받침한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 의심 카운터"
|
||||
```bash label="[lab host] ① 의심 카운터"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_jgroups_fd_sock2_get_num_suspected_members'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 병합 횟수"
|
||||
```bash label="[lab host] ② 병합 횟수"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_jgroups_merge3_get_num_merge_events'
|
||||
```
|
||||
@@ -641,16 +680,16 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
|
||||
### 5. 양방향으로 막으면 갈라진다
|
||||
|
||||
```bash label="[kc-lab-1] ① 25초 간격으로 파드"
|
||||
```bash label="[lab host] ① 25초 간격으로 파드"
|
||||
kubectl -n keycloak-lab get pods | grep keycloak
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② Service 에서 빠졌는지"
|
||||
```bash label="[lab host] ② Service 에서 빠졌는지"
|
||||
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
|
||||
-o custom-columns=ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
|
||||
-o "custom-columns=ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 같은 간격으로 밖에서"
|
||||
```bash label="[lab host] ③ 같은 간격으로 밖에서"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
|
||||
@@ -675,7 +714,7 @@ curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.co
|
||||
|
||||
### 6. split brain 은 DB 한 줄로 확인한다
|
||||
|
||||
```bash label="[kc-lab-1] ① 앞에서 친 것과 같은 쿼리"
|
||||
```bash label="[lab host] ① 앞에서 친 것과 같은 쿼리"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select name, ip, coord from jgroups_ping order by name"
|
||||
```
|
||||
@@ -694,16 +733,16 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
Keycloak 컨테이너에 `curl` 이 없으므로 상주 파드를 띄운다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 상주 탐침을 띄운다"
|
||||
```bash label="[lab host] ① 상주 탐침을 띄운다"
|
||||
kubectl -n keycloak-lab run a5-probe --image=curlimages/curl:8.11.1 \
|
||||
--restart=Never --command -- sleep 1800
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 뜰 때까지 기다린다"
|
||||
```bash label="[lab host] ② 뜰 때까지 기다린다"
|
||||
kubectl -n keycloak-lab wait --for=condition=Ready pod/a5-probe --timeout=120s
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 두 노드의 헬스체크를 각각"
|
||||
```bash label="[lab host] ③ 두 노드의 헬스체크를 각각"
|
||||
kubectl -n keycloak-lab exec a5-probe -- curl -s "http://$K0:9000/health/ready"
|
||||
kubectl -n keycloak-lab exec a5-probe -- curl -s "http://$K1:9000/health/ready"
|
||||
```
|
||||
@@ -751,25 +790,25 @@ Keycloak 의 클러스터 헬스체크는 비대칭이다. 코디네이터였던
|
||||
|
||||
지우기 전에 무엇이 있는지 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 이 노드"
|
||||
sudo iptables -t raw -L PREROUTING -n -v --line-numbers
|
||||
```bash label="[lab host] ① kc-lab-1"
|
||||
ssh kc-lab-1 'sudo iptables -t raw -L PREROUTING -n -v --line-numbers'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 반대 노드"
|
||||
```bash label="[lab host] ② 반대 노드"
|
||||
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n -v --line-numbers'
|
||||
```
|
||||
|
||||
그다음 해제 시각을 찍고 양쪽을 비운다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 해제 시각"
|
||||
```bash label="[lab host] ③ 해제 시각"
|
||||
date '+%H:%M:%S 해제'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ④ 이 노드를 비운다"
|
||||
sudo iptables -t raw -F PREROUTING
|
||||
```bash label="[lab host] ④ kc-lab-1 을 비운다"
|
||||
ssh kc-lab-1 'sudo iptables -t raw -F PREROUTING'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ⑤ 반대 노드를 비운다"
|
||||
```bash label="[lab host] ⑤ 반대 노드를 비운다"
|
||||
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
|
||||
```
|
||||
|
||||
@@ -791,7 +830,7 @@ ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
|
||||
|
||||
### 2. 누가 붙였는지는 MergeView 가 말한다
|
||||
|
||||
```bash label="[kc-lab-1] ① 양쪽 로그의 마지막 병합"
|
||||
```bash label="[lab host] ① 양쪽 로그의 마지막 병합"
|
||||
kubectl -n keycloak-lab logs keycloak-0 | grep MergeView | tail -1
|
||||
kubectl -n keycloak-lab logs keycloak-1 | grep MergeView | tail -1
|
||||
```
|
||||
@@ -811,7 +850,7 @@ MergeView::[...|15] (2) ← 병합. 뷰 ID 는 계속 증가한다
|
||||
|
||||
뷰 ID 는 단조 증가하므로 언제 몇 번 갈라졌는지를 로그만으로 셀 수 있다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 캐시별 재분배 로그"
|
||||
```bash label="[lab host] ② 캐시별 재분배 로그"
|
||||
kubectl -n keycloak-lab logs keycloak-0 | grep ISPN100007 | tail -6
|
||||
```
|
||||
|
||||
@@ -848,7 +887,7 @@ kubectl -n keycloak-lab logs keycloak-0 | grep ISPN100007 | tail -6
|
||||
| 탐침 파드 | `kubectl -n keycloak-lab get pod a5-probe` | 지웠으면 `NotFound` |
|
||||
| 밖 | `curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master` | `200` |
|
||||
|
||||
```bash label="[kc-lab-1] 탐침 파드를 지운다"
|
||||
```bash label="[lab host] 탐침 파드를 지운다"
|
||||
kubectl -n keycloak-lab delete pod a5-probe --ignore-not-found
|
||||
```
|
||||
|
||||
|
||||
+81
-42
@@ -39,7 +39,9 @@ postgres 가 보내는 패킷 중 노드를 건너가는 것만 200밀리초 지
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
`kubectl` 은 `kc-lab-1` 에서 친다. `tc` 는 노드 자체를 건드리는 명령이라 `kc-lab-2` 에서 치고, postgres 가 그 노드에 있다. 아래 명령들은 가이드가 실제로 친 한 줄 형태 그대로이므로 `ssh kc-lab-2 '...'` 가 붙어 있다. 코드블록마다 어느 셸인지 붙여 두었다.
|
||||
`kubectl` 은 `[lab host]` 에서 친다. `tc` 는 노드 자체를 건드리는 명령이라 `kc-lab-2` 에서 치고, postgres 가 그 노드에 있다. 아래 명령들은 가이드가 실제로 친 한 줄 형태 그대로이므로 `ssh kc-lab-2 '...'` 가 붙어 있다. 코드블록마다 어느 셸인지 붙여 두었다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
터미널은 둘을 연다. 하나는 부하와 측정, 하나는 이벤트 관찰이다.
|
||||
|
||||
@@ -83,13 +85,13 @@ A-2 는 DB 를 완전히 세웠고 A-4 는 기계를 통째로 껐다. 둘 다
|
||||
|
||||
**지금 치는 명령이 아니다.** 아래 한 줄은 중간에 그만둘 때 치는 것이고, 여기서는 어디 있는지만 봐 둔다. 아직 아무것도 걸지 않았으므로 지금 치면 지울 것이 없다.
|
||||
|
||||
```bash label="[kc-lab-1] 중간에 그만둘 때 치는 한 줄 — 읽어만 둔다"
|
||||
```bash label="[lab host] 중간에 그만둘 때 치는 한 줄 — 읽어만 둔다"
|
||||
ssh kc-lab-2 'sudo tc qdisc del dev flannel.1 root'
|
||||
```
|
||||
|
||||
지금 쳐서 확인할 것은 걸린 것이 없다는 쪽이다.
|
||||
|
||||
```bash label="[kc-lab-1] 지금 flannel.1 에 무엇이 걸려 있는지 본다"
|
||||
```bash label="[lab host] 지금 flannel.1 에 무엇이 걸려 있는지 본다"
|
||||
ssh kc-lab-2 'sudo tc qdisc show dev flannel.1'
|
||||
```
|
||||
|
||||
@@ -111,11 +113,11 @@ ssh kc-lab-2 'sudo tc qdisc show dev flannel.1'
|
||||
|
||||
**무엇을 확인하는가** — postgres 와 `keycloak-0` 이 같은 노드이고 `keycloak-1` 만 노드를 건너는지.
|
||||
|
||||
```bash label="[kc-lab-1] ① 배치와 IP"
|
||||
```bash label="[lab host] ① 배치와 IP"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 뒤에서 계속 쓸 세 값을 셸 변수에 담는다"
|
||||
```bash label="[lab host] ② 뒤에서 계속 쓸 세 값을 셸 변수에 담는다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
PG=$(kubectl -n keycloak-lab get pod -l app=postgres -o jsonpath='{.items[0].status.podIP}')
|
||||
@@ -148,7 +150,7 @@ echo "K0=$K0 K1=$K1 PG=$PG"
|
||||
|
||||
**목적** — 같은 요청을 수십 번 반복할 수 있는 파드를 하나 띄우고, 비밀번호를 값으로 찍지 않고 넘긴다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 탐침 파드를 띄운다"
|
||||
```bash label="[lab host] ① 탐침 파드를 띄운다"
|
||||
kubectl -n keycloak-lab run a6-probe --image=curlimages/curl:8.11.1 \
|
||||
--restart=Never \
|
||||
--env="K0=$K0" --env="K1=$K1" \
|
||||
@@ -157,11 +159,11 @@ kubectl -n keycloak-lab run a6-probe --image=curlimages/curl:8.11.1 \
|
||||
--command -- sleep 1800
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 뜰 때까지 기다린다"
|
||||
```bash label="[lab host] ② 뜰 때까지 기다린다"
|
||||
kubectl -n keycloak-lab wait --for=condition=Ready pod/a6-probe --timeout=120s
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 값이 아니라 길이만 확인한다"
|
||||
```bash label="[lab host] ③ 값이 아니라 길이만 확인한다"
|
||||
kubectl -n keycloak-lab exec a6-probe -- sh -c 'echo "K0=$K0 K1=$K1 PW=${#PW}자"'
|
||||
```
|
||||
|
||||
@@ -179,7 +181,7 @@ K0=10.42.1.77 K1=10.42.0.42 PW=32자
|
||||
|
||||
**무엇을 확인하는가** — 시간이 연결에 드는지 첫 바이트까지 드는지.
|
||||
|
||||
```bash label="[kc-lab-1] ① 구간별 시간을 함께 찍는다"
|
||||
```bash label="[lab host] ① 구간별 시간을 함께 찍는다"
|
||||
kubectl -n keycloak-lab exec a6-probe -- sh -c '
|
||||
curl -s -o /dev/null \
|
||||
-w "connect %{time_connect} ttfb %{time_starttransfer} total %{time_total}\n" \
|
||||
@@ -204,7 +206,7 @@ connect 0.001 ttfb 0.065 total 0.066
|
||||
|
||||
**무엇을 확인하는가** — 주입 전 두 노드의 응답 시간.
|
||||
|
||||
```bash label="[kc-lab-1] ① keycloak-1 에 20회"
|
||||
```bash label="[lab host] ① keycloak-1 에 20회"
|
||||
kubectl -n keycloak-lab exec a6-probe -- sh -c '
|
||||
rm -f /tmp/base-k1 ; i=0
|
||||
while [ $i -lt 20 ]; do
|
||||
@@ -216,16 +218,16 @@ kubectl -n keycloak-lab exec a6-probe -- sh -c '
|
||||
done'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 원본을 먼저 본다"
|
||||
```bash label="[lab host] ② 원본을 먼저 본다"
|
||||
kubectl -n keycloak-lab exec a6-probe -- cat /tmp/base-k1
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 그다음 줄여서 본다"
|
||||
```bash label="[lab host] ③ 그다음 줄여서 본다"
|
||||
kubectl -n keycloak-lab exec a6-probe -- cat /tmp/base-k1 \
|
||||
| awk '{s+=$1} END {printf "%d회 평균 %.0f ms\n", NR, s*1000/NR}'
|
||||
```
|
||||
|
||||
`$K1` 을 `$K0` 로 바꿔 대조군도 똑같이 잰다.
|
||||
`$K1` 을 `$K0` 로 바꿔 대조군도 똑같이 잰다. **`/tmp/base-k1` 이라는 출력 파일 이름도 같이 바꾼다** — ① 은 그 경로에 덮어쓰므로, 변수만 바꾸고 다시 돌리면 `keycloak-1` 에서 잰 20회가 지워지고 `keycloak-0` 의 20회가 그 파일을 차지한다. 화면에는 아무 경고도 안 뜬다. 뒤에서 두 노드의 평균을 견줄 때 `keycloak-1` 쪽이 이미 없다.
|
||||
|
||||
**출력에서 답이 되는 것** — 두 노드의 평균이다.
|
||||
|
||||
@@ -241,7 +243,7 @@ kubectl -n keycloak-lab exec a6-probe -- cat /tmp/base-k1 \
|
||||
|
||||
**무엇을 확인하는가** — `agroal_*` 지표의 이름과 지금 값.
|
||||
|
||||
```bash label="[kc-lab-1] ① 지표 이름 목록"
|
||||
```bash label="[lab host] ① 지표 이름 목록"
|
||||
kubectl -n keycloak-lab exec a6-probe -- sh -c \
|
||||
'curl -s "http://$K1:9000/metrics" | grep "^agroal_"'
|
||||
```
|
||||
@@ -280,7 +282,7 @@ agroal_destroy_count_total
|
||||
|
||||
**목적** — 인터넷 예제가 전부 쓰는 이름을 그대로 써 보고 무엇이 나오는지 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 예제 그대로"
|
||||
```bash label="[lab host] ① 예제 그대로"
|
||||
ssh kc-lab-2 'sudo tc qdisc add dev eth0 root handle 1: prio'
|
||||
```
|
||||
|
||||
@@ -307,7 +309,7 @@ Cannot find device "eth0"
|
||||
|
||||
**문제가 생기면** — 인터페이스 이름을 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 이 게스트에 무엇이 있나"
|
||||
```bash label="[lab host] ② 이 게스트에 무엇이 있나"
|
||||
ssh kc-lab-2 'ip -brief link'
|
||||
```
|
||||
|
||||
@@ -356,11 +358,11 @@ Debian 클라우드 이미지는 예측 가능한 인터페이스 이름을 쓴
|
||||
|
||||
`enp1s0` 에서 `match ip src 10.42.1.76` 은 절대 일치하지 않는다. 그 IP 는 페이로드 안에 있고 헤더에는 노드 IP 만 있다. 눈으로 확인하는 두 줄을 가이드가 미검증으로 표시했다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 미검증 — 물리 쪽에는 노드 IP 만 보인다"
|
||||
```bash label="[lab host] ① 미검증 — 물리 쪽에는 노드 IP 만 보인다"
|
||||
ssh kc-lab-2 'sudo tcpdump -i enp1s0 -n -c 5 udp port 8472'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 미검증 — 터널 쪽에는 파드 IP 가 보인다"
|
||||
```bash label="[lab host] ② 미검증 — 터널 쪽에는 파드 IP 가 보인다"
|
||||
ssh kc-lab-2 'sudo tcpdump -i flannel.1 -n -c 5 host 10.42.1.76'
|
||||
```
|
||||
|
||||
@@ -380,22 +382,51 @@ ssh kc-lab-2 'sudo tcpdump -i flannel.1 -n -c 5 host 10.42.1.76'
|
||||
|
||||
**목적** — postgres 가 보내는 패킷만 골라 200ms 지연시킨다.
|
||||
|
||||
2026-09-17 에 새 실험대에서 이 세 줄을 쳐서 같은 수를 받았다(observed). 넣기 전과 뒤를 같은 명령으로 쟀다.
|
||||
|
||||
```text
|
||||
넣기 전 200 0.067946s 200 0.064262s
|
||||
넣은 뒤 200 1.905504s 200 1.903161s 200 1.860396s 200 1.867929s 200 1.872740s
|
||||
되돌린 뒤 200 0.067775s 200 0.043802s 200 0.045763s
|
||||
```
|
||||
|
||||
**200ms 를 넣었는데 1.87초가 된다.** 문서가 적은 `1,872ms` 와 28배가 그대로 나왔다. 그 사이 `netem` 카운터는 `83 pkt` 이었고, 되돌리자 `qdisc noqueue` 로 돌아가며 시간도 원래대로였다.
|
||||
|
||||
시도 ① 과 ② 의 관측도 다시 나왔다(observed).
|
||||
|
||||
```text
|
||||
sudo tc qdisc add dev eth0 root handle 1: prio → Cannot find device "eth0"
|
||||
enp1s0 의 헤더 192.168.122.12.53654 > 192.168.122.11.8472: OTV … ← 노드 IP 뿐
|
||||
flannel.1 의 헤더 10.42.0.14.52916 > 10.42.1.29.5432 … ← 파드 IP 가 보인다
|
||||
```
|
||||
|
||||
**문서가 미검증으로 둔 세 줄도 전부 돌았다**(observed) — `tcpdump` 두 줄과 `tc filter show`. 필터 목록은 이렇게 나온다.
|
||||
|
||||
```text
|
||||
filter parent 1: protocol ip pref 3 u32 chain 0 fh 800::800 order 2048 key ht 800 bkt 0 *flowid 1:3 not_in_hw
|
||||
match 0a2a011d/ffffffff at 12
|
||||
```
|
||||
|
||||
`0a2a011d` 가 `10.42.1.29` 를 16진수로 적은 것이고, 그 값이 지금 postgres 의 IP 와 같은지가 필터가 제 대상을 고르고 있다는 증거다.
|
||||
|
||||
**`tcpdump` 에는 시간 제한을 걸어 둔다.** `-c 5` 는 패킷 다섯 개를 받아야 끝나므로, 그 조건에 맞는 트래픽이 없으면 명령이 안 끝난다. `sudo timeout 12 tcpdump …` 로 감싸면 조용한 구간에서도 프롬프트가 돌아온다.
|
||||
|
||||
한 줄씩 친다. 앞 줄이 실패하면 뒤 줄은 붙을 곳이 없어서 다른 에러를 낸다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 밴드 3개짜리 분류기를 만든다"
|
||||
```bash label="[lab host] ① 밴드 3개짜리 분류기를 만든다"
|
||||
ssh kc-lab-2 "sudo tc qdisc add dev flannel.1 root handle 1: prio"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 3번 밴드에 200ms 지연을 붙인다"
|
||||
```bash label="[lab host] ② 3번 밴드에 200ms 지연을 붙인다"
|
||||
ssh kc-lab-2 "sudo tc qdisc add dev flannel.1 parent 1:3 handle 30: netem delay 200ms"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 출발지가 postgres 인 패킷을 3번 밴드로 보낸다"
|
||||
```bash label="[lab host] ③ 출발지가 postgres 인 패킷을 3번 밴드로 보낸다"
|
||||
ssh kc-lab-2 "sudo tc filter add dev flannel.1 protocol ip parent 1:0 prio 3 \
|
||||
u32 match ip src $PG/32 flowid 1:3"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ④ 주입 시각"
|
||||
```bash label="[lab host] ④ 주입 시각"
|
||||
date '+%H:%M:%S 주입'
|
||||
```
|
||||
|
||||
@@ -438,7 +469,7 @@ date '+%H:%M:%S 주입'
|
||||
|
||||
### 2. 성공한 주입 뒤에는 카운터를 본다
|
||||
|
||||
```bash label="[kc-lab-1] ① 넣은 직후의 카운터"
|
||||
```bash label="[lab host] ① 넣은 직후의 카운터"
|
||||
ssh kc-lab-2 'sudo tc -s qdisc show dev flannel.1'
|
||||
```
|
||||
|
||||
@@ -453,14 +484,14 @@ qdisc netem 30: parent 1:3 limit 1000 delay 200ms
|
||||
|
||||
`Sent 0 pkt` 인데 이것은 실패가 아니다. A-5 에서 `pkts 0` 은 규칙이 안 걸렸다는 뜻이었고, 여기서는 아직 아무 패킷도 지나가지 않았을 뿐이다. postgres 는 요청이 있어야 답하므로 트래픽을 한 번 만든다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 요청을 한 번 보낸다"
|
||||
```bash label="[lab host] ② 요청을 한 번 보낸다"
|
||||
kubectl -n keycloak-lab exec a6-probe -- sh -c '
|
||||
curl -s -o /dev/null -w "%{time_total}\n" \
|
||||
-X POST "http://$K1:8080/realms/master/protocol/openid-connect/token" \
|
||||
-d grant_type=password -d client_id=admin-cli -d username=admin -d "password=$PW"'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 다시 센다"
|
||||
```bash label="[lab host] ③ 다시 센다"
|
||||
ssh kc-lab-2 'sudo tc -s qdisc show dev flannel.1 | grep -A2 netem'
|
||||
```
|
||||
|
||||
@@ -482,7 +513,7 @@ ssh kc-lab-2 'sudo tc -s qdisc show dev flannel.1 | grep -A2 netem'
|
||||
|
||||
A-1 과 A-5 에서 나온 것과 같은 교훈이 세 번째로 나왔다 — 주입을 넣은 것과 걸린 것은 다르다. 필터 자체를 보는 줄은 가이드가 미검증으로 표시했다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 미검증 — 필터 목록"
|
||||
```bash label="[lab host] ④ 미검증 — 필터 목록"
|
||||
ssh kc-lab-2 'sudo tc filter show dev flannel.1'
|
||||
```
|
||||
|
||||
@@ -492,7 +523,7 @@ ssh kc-lab-2 'sudo tc filter show dev flannel.1'
|
||||
|
||||
주입 전에 친 것과 똑같은 명령을 다시 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 구간별 시간"
|
||||
```bash label="[lab host] ① 구간별 시간"
|
||||
kubectl -n keycloak-lab exec a6-probe -- sh -c '
|
||||
curl -s -o /dev/null \
|
||||
-w "connect %{time_connect} ttfb %{time_starttransfer} total %{time_total}\n" \
|
||||
@@ -536,7 +567,7 @@ COMMIT
|
||||
|
||||
순차로 20번 돌리면 큐잉이 재현되지 않는다. 백그라운드로 띄우고 `wait` 하며, 결과는 파드 안 파일에 모은다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 동시 20건"
|
||||
```bash label="[lab host] ① 동시 20건"
|
||||
kubectl -n keycloak-lab exec a6-probe -- sh -c '
|
||||
rm -f /tmp/load ; i=0
|
||||
while [ $i -lt 20 ]; do
|
||||
@@ -549,16 +580,16 @@ kubectl -n keycloak-lab exec a6-probe -- sh -c '
|
||||
wait'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 다 모였는지부터 센다"
|
||||
```bash label="[lab host] ② 다 모였는지부터 센다"
|
||||
kubectl -n keycloak-lab exec a6-probe -- cat /tmp/load > /tmp/load.txt
|
||||
wc -l /tmp/load.txt
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 원본을 본다"
|
||||
```bash label="[lab host] ③ 원본을 본다"
|
||||
cat /tmp/load.txt
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ④ 상태 코드와 시간을 나눠 본다"
|
||||
```bash label="[lab host] ④ 상태 코드와 시간을 나눠 본다"
|
||||
awk '{print $1}' /tmp/load.txt | sort | uniq -c
|
||||
awk '{print $2}' /tmp/load.txt | sort -g
|
||||
```
|
||||
@@ -591,12 +622,20 @@ awk '{print $2}' /tmp/load.txt | sort -g
|
||||
|
||||
순서가 이상하다. `10.5` 가 `3.4` 보다 앞에 있는데 원 실행이 `sort` 를 사전순으로 썼기 때문이다(맨 앞의 `1` 은 `uniq -c` 가 붙인 개수다). 문자열로 정렬하면 `"10.5" < "3.4"` 다.
|
||||
|
||||
```bash label="[kc-lab-1] ⑤ 정렬 방식을 갈라 본다"
|
||||
```bash label="[lab host] ⑤ 정렬 방식을 갈라 본다"
|
||||
sort /tmp/load.txt # 사전순 — 10.5 가 3.4 앞에 온다
|
||||
sort -g /tmp/load.txt # 수치순 — 이걸 써야 한다
|
||||
```
|
||||
|
||||
시간 값을 정렬할 때는 `sort -g` 를 쓴다. 이걸 놓치면 최대값을 잘못 읽는다. 숫자를 순서대로 놓으면 계단이 된다.
|
||||
**원 가이드의 저 처방은 이 파일에서 안 듣는다.** 각 줄이 `200 1.911191` 처럼 전부 같은 `200` 으로 시작해서, `sort -g` 는 줄 앞머리 수치를 비교하다 전부 동률이 되고 마지막에 바이트 순으로 떨어진다. `sort` 와 출력이 똑같다. 정렬할 열을 짚어야 듣는다.
|
||||
|
||||
```bash label="[lab host] ⑥ 열을 짚어 다시 정렬한다"
|
||||
sort -g -k2 /tmp/load.txt
|
||||
```
|
||||
|
||||
위 ③ 의 `awk '{print $2}' /tmp/load.txt | sort -g` 도 제대로 돈다 — `awk` 가 둘째 열만 남기기 때문이다. 최대값 하나만 볼 때는 거기에 `| tail -1` 을 붙인다.
|
||||
|
||||
시간 값을 정렬할 때는 **열을 짚는다.** 이걸 놓치면 최대값을 잘못 읽고, `sort -g` 를 붙였다는 것만으로 고쳤다고 믿게 된다. 숫자를 순서대로 놓으면 계단이 된다.
|
||||
|
||||
```text
|
||||
1.9 → 3.4 → 4.8 → 6.2 → 7.7 → 9.1 → 10.5 → ... → 22.2
|
||||
@@ -608,7 +647,7 @@ sort -g /tmp/load.txt # 수치순 — 이걸 써야 한다
|
||||
|
||||
### 3. 커넥션 풀 지표는 부하가 끝나자마자 읽는다
|
||||
|
||||
```bash label="[kc-lab-1] ① 부하 직후에 읽는다"
|
||||
```bash label="[lab host] ① 부하 직후에 읽는다"
|
||||
kubectl -n keycloak-lab exec a6-probe -- sh -c \
|
||||
'curl -s "http://$K1:9000/metrics" | grep -E "^agroal_(blocking_time|max_used|acquire|active|available|awaiting)"'
|
||||
```
|
||||
@@ -642,11 +681,11 @@ histogram_quantile(0.99, rate(http_server_requests_seconds_bucket[5m]))
|
||||
|
||||
### 4. 헬스체크가 같은 줄에 선다
|
||||
|
||||
```bash label="[kc-lab-1] ① 최근 이벤트"
|
||||
```bash label="[lab host] ① 최근 이벤트"
|
||||
kubectl -n keycloak-lab get events --sort-by=.lastTimestamp | tail -20
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드 상태"
|
||||
```bash label="[lab host] ② 파드 상태"
|
||||
kubectl -n keycloak-lab get pods
|
||||
```
|
||||
|
||||
@@ -702,7 +741,7 @@ readiness 프로브 자체가 타임아웃됐다. 헬스체크도 같은 커넥
|
||||
|
||||
지연 구간의 로그를 세는 줄을 가이드가 미검증으로 표시했다. 원 실행의 정확한 패턴이 기록에 없다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 미검증 — 충돌 로그를 센다"
|
||||
```bash label="[lab host] ① 미검증 — 충돌 로그를 센다"
|
||||
kubectl -n keycloak-lab logs keycloak-1 --since=20m \
|
||||
| grep -icE 'optimistic|StaleState|version.*conflict'
|
||||
```
|
||||
@@ -727,15 +766,15 @@ kubectl -n keycloak-lab logs keycloak-1 --since=20m \
|
||||
|
||||
**목적** — `flannel.1` 의 `root` qdisc 를 지워 `prio` 와 `netem` 과 filter 를 한꺼번에 없앤다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 해제 시각"
|
||||
```bash label="[lab host] ① 해제 시각"
|
||||
date '+%H:%M:%S 해제'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② root 를 지운다"
|
||||
```bash label="[lab host] ② root 를 지운다"
|
||||
ssh kc-lab-2 'sudo tc qdisc del dev flannel.1 root'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 무엇이 남았는지 본다"
|
||||
```bash label="[lab host] ③ 무엇이 남았는지 본다"
|
||||
ssh kc-lab-2 'sudo tc qdisc show dev flannel.1'
|
||||
```
|
||||
|
||||
@@ -775,7 +814,7 @@ keycloak-1 1/1 Running 1 (52m ago) 3h24m
|
||||
|
||||
**문제가 생기면** — 탐침 파드를 지운다.
|
||||
|
||||
```bash label="[kc-lab-1] 탐침 파드를 지운다"
|
||||
```bash label="[lab host] 탐침 파드를 지운다"
|
||||
kubectl -n keycloak-lab delete pod a6-probe --ignore-not-found
|
||||
```
|
||||
|
||||
@@ -803,7 +842,7 @@ kubectl -n keycloak-lab delete pod a6-probe --ignore-not-found
|
||||
| 지연이 양쪽 다 늘었다 | `netem` 을 `root` 에 직접 붙였다 | `prio` + `filter` 로 골라 낸다 |
|
||||
| `kubectl` 이나 SSH 까지 느려졌다 | `enp1s0` 에 걸었다 | `tc qdisc del dev enp1s0 root` |
|
||||
| 20줄 중 몇 줄만 온다 | `kubectl run --rm -i` 로 동시 실행하면 stdout 이 샌다 | 상주 파드 + 파일 |
|
||||
| 최대값이 `9.1` 로 보인다 | `sort` 가 사전순이다 | `sort -g` |
|
||||
| 최대값이 `9.1` 로 보인다 | `sort` 가 사전순이고, `sort -g` 만 붙여도 줄이 전부 `200` 으로 시작해 안 듣는다 | `sort -g -k2`, 또는 ③ 의 `awk` 형태 |
|
||||
| `blocking_time` 이 0 이다 | 부하가 끝나고 한참 뒤에 읽었다 | 부하 직후에 읽는다 |
|
||||
| `awaiting_count` 가 늘 0 이다 | 순간값이다 | 부하가 도는 중에 읽는다 |
|
||||
| 로그인이 전부 `401` | `PW` 가 안 넘어갔다 | `exec a6-probe -- sh -c 'echo ${#PW}'` |
|
||||
|
||||
+50
-32
@@ -42,7 +42,11 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 `kc-lab-1` 에서 친다. `kubectl` 에 `sudo` 를 붙이지 않는다 — 가이드가 「kubeconfig 를 사용자 홈에 복사해 뒀다면 `sudo` 는 빼도 된다」고 스스로 괄호를 달아 두었다. 마지막 한 단계만 호스트(`test-server`)로 넘어가고, 거기서는 사람이 비밀번호를 친다.
|
||||
명령은 `[lab host]` 에서 친다. `kubectl` 에 `sudo` 를 붙이지 않는다 — 가이드가 「kubeconfig 를 사용자 홈에 복사해 뒀다면 `sudo` 는 빼도 된다」고 스스로 괄호를 달아 두었다. 마지막 한 단계만 호스트(`test-server`)로 넘어가고, 거기서는 사람이 비밀번호를 친다.
|
||||
|
||||
**`[kc-lab-1]` 라벨이 붙은 블록은 게스트 셸이다.** lab host 에서 `ssh kc-lab-1` 로 들어가서 치고, 끝나면 `exit` 로 나온다. `ssh kc-lab-2 '…'` 한 줄 형태는 lab host 에서 그대로 쳐도 되므로 `[lab host]` 로 두었다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -88,7 +92,7 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
되돌리기는 한 줄이고, 파괴하기 전에 읽어 둔다.
|
||||
|
||||
```bash label="[kc-lab-1] 파괴하기 전에 읽어 두는 되돌리기 한 줄"
|
||||
```bash label="[lab host] 파괴하기 전에 읽어 두는 되돌리기 한 줄"
|
||||
kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
< /tmp/keycloak-backup.sql
|
||||
```
|
||||
@@ -107,7 +111,7 @@ kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak
|
||||
|
||||
**무엇을 보는가** — 파드 넷의 상태와 배치.
|
||||
|
||||
```bash label="[kc-lab-1] 파드 배치를 본다"
|
||||
```bash label="[lab host] 파드 배치를 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
|
||||
@@ -126,7 +130,7 @@ keycloak-1 1/1 Running 0 4m38s
|
||||
|
||||
**무엇을 보는가** — realm·client·user·세션의 개수. 처음 한 번은 읽는 형태로 친다. 값만 뽑는 형태부터 배우면 `psql` 이 무엇을 돌려주는지 모르게 된다.
|
||||
|
||||
```bash label="[kc-lab-1] ① psql 이 무엇을 돌려주는지 한 번 본다"
|
||||
```bash label="[lab host] ① psql 이 무엇을 돌려주는지 한 번 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select count(*) from realm"
|
||||
```
|
||||
@@ -144,7 +148,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
이제 넷을 한 줄로 모은다. 비교할 값이 필요할 때만 이 형태를 쓴다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 대조할 한 줄을 뽑는다"
|
||||
```bash label="[lab host] ② 대조할 한 줄을 뽑는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select (select count(*) from realm), (select count(*) from client),
|
||||
(select count(*) from user_entity),
|
||||
@@ -165,7 +169,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
**무엇을 보는가** — 질문 ①의 재료. 세션 행이 실제로 테이블에 있어야 덤프에 들어갈 것이 있다.
|
||||
|
||||
```bash label="[kc-lab-1] 세션 행을 나열한다"
|
||||
```bash label="[lab host] 세션 행을 나열한다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select user_session_id, offline_flag, realm_id from offline_user_session"
|
||||
```
|
||||
@@ -189,13 +193,13 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**무엇을 보는가** — 정문과 app1 의 응답. 처음 한 번은 응답을 읽는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 헤더를 통째로 본다"
|
||||
```bash label="[lab host] ① 헤더를 통째로 본다"
|
||||
curl -I https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
|
||||
헤더가 통째로 나온다. `HTTP/2 200`, `content-type: application/json` 을 본다. 같은 것을 반복해서 재고 비교할 때만 코드만 뽑는다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 코드만 뽑아 둘을 잰다"
|
||||
```bash label="[lab host] ② 코드만 뽑아 둘을 잰다"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://app1.hyeonworks.com/
|
||||
```
|
||||
@@ -215,7 +219,7 @@ curl -s -o /dev/null -w '%{http_code}\n' https://app1.hyeonworks.com/
|
||||
|
||||
① 시각을 남기고 덤프를 뜬다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 시각과 함께 덤프를 뜬다"
|
||||
```bash label="[lab host] ① 시각과 함께 덤프를 뜬다"
|
||||
date '+%H:%M:%S 백업 시작'
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
|
||||
--clean --if-exists > /tmp/keycloak-backup.sql
|
||||
@@ -243,7 +247,7 @@ date '+%H:%M:%S 백업 완료'
|
||||
|
||||
**문제가 생기면** — 이 단계는 읽기만 하므로 파일이 마음에 안 들면 지우고 다시 뜬다.
|
||||
|
||||
```bash label="[kc-lab-1] 덤프가 마음에 안 들면 지우고 다시 뜬다"
|
||||
```bash label="[lab host] 덤프가 마음에 안 들면 지우고 다시 뜬다"
|
||||
rm -f /tmp/keycloak-backup.sql
|
||||
```
|
||||
|
||||
@@ -253,7 +257,7 @@ rm -f /tmp/keycloak-backup.sql
|
||||
|
||||
① 크기와 줄 수.
|
||||
|
||||
```bash label="[kc-lab-1] ① 크기와 줄 수"
|
||||
```bash label="[lab host] ① 크기와 줄 수"
|
||||
ls -l /tmp/keycloak-backup.sql
|
||||
wc -l /tmp/keycloak-backup.sql
|
||||
```
|
||||
@@ -266,7 +270,7 @@ wc -l /tmp/keycloak-backup.sql
|
||||
|
||||
② 테이블 수.
|
||||
|
||||
```bash label="[kc-lab-1] ② 덤프 안의 테이블 수"
|
||||
```bash label="[lab host] ② 덤프 안의 테이블 수"
|
||||
grep -c '^CREATE TABLE' /tmp/keycloak-backup.sql
|
||||
```
|
||||
|
||||
@@ -280,7 +284,7 @@ grep -c '^CREATE TABLE' /tmp/keycloak-backup.sql
|
||||
|
||||
③ 끝까지 쓰였는가.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 마지막 세 줄"
|
||||
```bash label="[lab host] ③ 마지막 세 줄"
|
||||
tail -3 /tmp/keycloak-backup.sql
|
||||
```
|
||||
|
||||
@@ -296,7 +300,7 @@ tail -3 /tmp/keycloak-backup.sql
|
||||
|
||||
④ 세션이 들어갔는가. 이것이 질문 ① 자체다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ COPY 블록에 세션 행이 붙어 있는가"
|
||||
```bash label="[lab host] ④ COPY 블록에 세션 행이 붙어 있는가"
|
||||
grep -c 'offline_user_session' /tmp/keycloak-backup.sql
|
||||
grep -A3 'COPY public.offline_user_session' /tmp/keycloak-backup.sql | cut -c1-110
|
||||
```
|
||||
@@ -320,7 +324,7 @@ grep -A3 'COPY public.offline_user_session' /tmp/keycloak-backup.sql | cut -c1-1
|
||||
|
||||
**무엇을 보는가** — 파일의 경로와 그 파일이 올라앉은 디스크.
|
||||
|
||||
```bash label="[kc-lab-1] 덤프의 경로와 디스크를 본다"
|
||||
```bash label="[lab host] 덤프의 경로와 디스크를 본다"
|
||||
ls -l /tmp/keycloak-backup.sql
|
||||
df -h /tmp
|
||||
```
|
||||
@@ -337,7 +341,7 @@ df -h /tmp
|
||||
|
||||
① 시각을 남기고 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 시각을 남기고 스키마를 지운다"
|
||||
```bash label="[lab host] ① 시각을 남기고 스키마를 지운다"
|
||||
date '+%H:%M:%S 파괴'
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
|
||||
@@ -362,12 +366,12 @@ CREATE SCHEMA
|
||||
|
||||
결과를 해석하기 전에, 의도한 것만 지워졌는지 먼저 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 남은 테이블을 센다"
|
||||
```bash label="[lab host] ① 남은 테이블을 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select count(*) from pg_tables where schemaname='public'"
|
||||
```
|
||||
|
||||
이 실험대는 스크립트로 셌고(observed), 위 형태는 가이드가 미검증으로 표시한 줄이다(unknown). 결과는 이렇다(observed).
|
||||
이 실험대는 스크립트로 셌고(observed), **위 형태도 2026-09-17 에 쳐서 `0` 을 받았다**(observed). 결과는 이렇다(observed).
|
||||
|
||||
```text
|
||||
남은 테이블: 0
|
||||
@@ -377,7 +381,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
애플리케이션 테이블이 정말 없는지 직접 물어본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 테이블에 직접 물어본다"
|
||||
```bash label="[lab host] ② 테이블에 직접 물어본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select count(*) from realm"
|
||||
```
|
||||
@@ -394,7 +398,7 @@ LINE 1: select count(*) from realm
|
||||
|
||||
**그런데 밖은 멀쩡하다.**
|
||||
|
||||
```bash label="[kc-lab-1] ③ 파드와 밖에서 본 상태를 다시 잰다"
|
||||
```bash label="[lab host] ③ 파드와 밖에서 본 상태를 다시 잰다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://app1.hyeonworks.com/
|
||||
@@ -413,9 +417,9 @@ keycloak-1 1/1 Running 0 4m38s
|
||||
|
||||
엉뚱한 것을 죽이지 않았는지도 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ Service 에서 빠진 파드가 있는가"
|
||||
```bash label="[lab host] ④ Service 에서 빠진 파드가 있는가"
|
||||
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
|
||||
-o custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
|
||||
-o "custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready"
|
||||
```
|
||||
|
||||
ready 주소가 여전히 둘이다. 아무 파드도 Service 에서 빠지지 않았다. A-2 에서는 여기가 빈 목록이었다. `kubectl get endpoints` 는 v1.33+ 에서 deprecated 이고, 이 실험대에서 실제로 그 경고를 봤다.
|
||||
@@ -424,7 +428,7 @@ ready 주소가 여전히 둘이다. 아무 파드도 Service 에서 빠지지
|
||||
|
||||
여기까지의 주입 검증은 쉽게 통과한다. 어려운 확인은 반대편에 있다. 복구 명령에서 `-i` 를 빠뜨리면 파드 안의 `psql` 이 빈 입력을 받고 정상 종료하고, 셸은 오류를 내지 않고, 종료 코드도 0 이며, `date` 두 줄은 「1초 만에 끝났다」로 찍힌다. 복구된 것과 구별되지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] 치지 않는다 — -i 가 있는 줄과 없는 줄을 눈으로 견준다"
|
||||
```bash label="[lab host] 치지 않는다 — -i 가 있는 줄과 없는 줄을 눈으로 견준다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql ... < dump.sql # ✘
|
||||
kubectl -n keycloak-lab exec -i deploy/postgres -- psql ... < dump.sql # ✔
|
||||
```
|
||||
@@ -435,7 +439,7 @@ kubectl -n keycloak-lab exec -i deploy/postgres -- psql ... < dump.sql # ✔
|
||||
|
||||
**전부 깨지지는 않는다.** 세 경로를 나눠서 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 두 경로를 잰다"
|
||||
```bash label="[lab host] ① 두 경로를 잰다"
|
||||
curl -s -o /dev/null -w 'certs %{http_code}\n' \
|
||||
https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs
|
||||
curl -s -o /dev/null -w 'well-known %{http_code}\n' \
|
||||
@@ -452,7 +456,7 @@ curl -s -o /dev/null -w 'well-known %{http_code}\n' \
|
||||
|
||||
토큰 발급은 값이 필요하므로 따로 친다. 이 실험대는 스크립트로 돌렸고(observed), 아래는 가이드가 미검증으로 표시한 형태다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② 토큰 발급을 잰다"
|
||||
```bash label="[lab host] ② 토큰 발급을 잰다"
|
||||
curl -s -o /dev/null -w '토큰 %{http_code}\n' -X POST \
|
||||
https://auth.hyeonworks.com/realms/master/protocol/openid-connect/token \
|
||||
-d grant_type=password -d client_id=admin-cli -d username=admin \
|
||||
@@ -462,7 +466,7 @@ curl -s -o /dev/null -w '토큰 %{http_code}\n' -X POST \
|
||||
|
||||
**비밀번호를 화면에 찍지 않는다.** 명령 치환으로 넘기므로 값은 터미널에도 셸 히스토리에도 남지 않는다. 길이만 확인하려면 한 줄을 더 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 값이 아니라 길이만 잰다"
|
||||
```bash label="[lab host] ③ 값이 아니라 길이만 잰다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
|
||||
```
|
||||
@@ -479,7 +483,7 @@ kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
|
||||
로그가 이유를 말한다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ Keycloak 로그를 읽는다"
|
||||
```bash label="[lab host] ④ Keycloak 로그를 읽는다"
|
||||
kubectl -n keycloak-lab logs keycloak-0 --tail=50
|
||||
```
|
||||
|
||||
@@ -516,7 +520,7 @@ kubectl -n keycloak-lab logs keycloak-0 --tail=50
|
||||
|
||||
① 시각을 남기고 복구한다. `-i` 가 있는지 치기 전에 눈으로 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 시각과 함께 덤프를 되돌린다"
|
||||
```bash label="[lab host] ① 시각과 함께 덤프를 되돌린다"
|
||||
date '+%H:%M:%S 복구 시작'
|
||||
kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
< /tmp/keycloak-backup.sql > /tmp/restore.log 2>&1
|
||||
@@ -533,7 +537,7 @@ date '+%H:%M:%S 복구 완료'
|
||||
|
||||
② 로그의 오류를 센다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 복구 로그의 오류를 센다"
|
||||
```bash label="[lab host] ② 복구 로그의 오류를 센다"
|
||||
grep -ci '^ERROR' /tmp/restore.log
|
||||
tail -5 /tmp/restore.log
|
||||
```
|
||||
@@ -542,6 +546,20 @@ tail -5 /tmp/restore.log
|
||||
|
||||
**왜 필요한가** — 시각 두 줄과 오류 0건은 `-i` 를 빠뜨렸을 때도 똑같이 나온다. 그래서 이 둘로는 복구를 판정하지 않는다.
|
||||
|
||||
**2026-09-17 에 이 절차를 처음부터 끝까지 쳤다**(observed).
|
||||
|
||||
```text
|
||||
덤프 101 테이블 · 976873 bytes · 8569 줄
|
||||
파괴 15:31:02 DROP SCHEMA · CREATE SCHEMA → 남은 테이블 0 · select count(*) from realm 이 relation does not exist
|
||||
그동안 밖에서는 200 ← 스키마가 통째로 없는데 정문은 멀쩡했다
|
||||
복구 15:31:02 → 15:31:08 (6초) · 복구 로그 ERROR 0
|
||||
복구 뒤 101 테이블 · 세션 6건 — 파괴 전과 같다
|
||||
```
|
||||
|
||||
**크기와 줄 수는 실험대마다 다르다.** 위 실측의 `394945 bytes · 6956 줄` 은 그 실험대의 값이고, BFF 쪽 테이블이 있는 이 실험대에서는 `976873 bytes · 8569 줄` 이었다. **같아야 하는 것은 테이블 수 `101` 과 복구 전후의 행 수**다.
|
||||
|
||||
**스키마가 없는 동안에도 정문이 `200` 인 것이 이 편의 핵심이다.** Keycloak 은 이미 읽어 둔 것으로 답하므로, 밖에서 보는 코드만으로는 데이터베이스가 통째로 비었다는 것을 알 수 없다.
|
||||
|
||||
**문제가 생기면** — 1초 만에 끝났는데 다음 단계의 대조가 어긋나면 `-i` 를 의심한다.
|
||||
|
||||
### 2. 진짜 판정 — 주입 전과 문자 단위로 견준다
|
||||
@@ -550,7 +568,7 @@ tail -5 /tmp/restore.log
|
||||
|
||||
① 주입 전에 친 것과 똑같은 명령을 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 대조할 한 줄을 다시 뽑는다"
|
||||
```bash label="[lab host] ① 대조할 한 줄을 다시 뽑는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select (select count(*) from realm), (select count(*) from client),
|
||||
(select count(*) from user_entity),
|
||||
@@ -574,7 +592,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
① 15초쯤 뒤에 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 15초 뒤에 다시 잰다"
|
||||
```bash label="[lab host] ① 15초 뒤에 다시 잰다"
|
||||
curl -s -o /dev/null -w 'well-known %{http_code}\n' \
|
||||
https://auth.hyeonworks.com/realms/keycloak-patterns/.well-known/openid-configuration
|
||||
kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||||
@@ -598,7 +616,7 @@ kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||||
|
||||
① 세션 행을 다시 나열한다. **주입 전 §3 에서 친 것과 열이 하나 다르다** — 거기는 `realm_id` 까지 셋을 뽑고 여기는 `user_session_id` 와 `offline_flag` 둘만 뽑는다. 가이드 원문이 그렇게 갈려 있어 그대로 싣는다. 열이 다르므로 행 수와 `user_session_id` 값으로 견준다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 세션 행을 다시 나열한다"
|
||||
```bash label="[lab host] ① 세션 행을 다시 나열한다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select user_session_id, offline_flag from offline_user_session"
|
||||
```
|
||||
|
||||
+67
-39
@@ -45,7 +45,9 @@ databasechangelog 행 수를 먼저 세고 이미지 태그를 정방향·롤백
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 `kc-lab-1` 에서 친다. `kubectl` 에 `sudo` 를 붙이지 않는다. 호스트로 넘어가는 단계가 없어 전 구간을 게스트 안에서 끝낸다. 터미널을 두 개 열어 두면 편하다 — 하나는 가용성 폴링용, 하나는 관찰용이다.
|
||||
명령은 `[lab host]` 에서 친다. `kubectl` 에 `sudo` 를 붙이지 않는다. 호스트로 넘어가는 단계가 없어 전 구간을 게스트 안에서 끝낸다. 터미널을 두 개 열어 두면 편하다 — 하나는 가용성 폴링용, 하나는 관찰용이다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -100,7 +102,7 @@ Keycloak 은 Liquibase 로 스키마를 관리한다. 적용한 변경 하나하
|
||||
|
||||
되돌리기는 전부 태그 한 줄이고, 각 단계 앞에서 먼저 읽는다.
|
||||
|
||||
```bash label="[kc-lab-1] 단계마다 먼저 읽어 두는 되돌리기 한 줄"
|
||||
```bash label="[lab host] 단계마다 먼저 읽어 두는 되돌리기 한 줄"
|
||||
kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
keycloak=quay.io/keycloak/keycloak:26.7.0
|
||||
```
|
||||
@@ -121,7 +123,7 @@ kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
|
||||
① 덤프를 뜨고 끝까지 쓰였는지 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 덤프를 뜨고 마지막 세 줄을 본다"
|
||||
```bash label="[lab host] ① 덤프를 뜨고 마지막 세 줄을 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
|
||||
--clean --if-exists > /tmp/pre-upgrade.sql
|
||||
ls -l /tmp/pre-upgrade.sql
|
||||
@@ -145,7 +147,7 @@ tail -3 /tmp/pre-upgrade.sql
|
||||
|
||||
**무엇을 보는가** — StatefulSet 에 적힌 태그.
|
||||
|
||||
```bash label="[kc-lab-1] ① StatefulSet 의 태그를 본다"
|
||||
```bash label="[lab host] ① StatefulSet 의 태그를 본다"
|
||||
kubectl -n keycloak-lab get statefulset keycloak \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].image}'; echo
|
||||
```
|
||||
@@ -158,19 +160,28 @@ quay.io/keycloak/keycloak:26.7.0
|
||||
|
||||
**이 값이 뜻하는 것** — `latest` 로 되어 있으면 무엇에서 무엇으로 가는지 말할 수 없기 때문에 이 실험이 성립하지 않는다. 그리고 StatefulSet 에 적힌 것과 파드가 실제로 돌리고 있는 것은 다를 수 있다 — 적용 중이거나 롤아웃이 멈춰 있으면 그렇다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드가 실제로 돌리는 이미지를 본다"
|
||||
kubectl -n keycloak-lab get pods -o custom-columns=\
|
||||
NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready,\
|
||||
RESTARTS:.status.containerStatuses[0].restartCount | grep keycloak
|
||||
```bash label="[lab host] ② 파드가 실제로 돌리는 이미지를 본다"
|
||||
kubectl -n keycloak-lab get pods \
|
||||
-o "custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount" \
|
||||
| grep keycloak
|
||||
```
|
||||
|
||||
두 파드의 `IMAGE` 가 서로 같고 StatefulSet 과도 같은지, `READY` 가 둘 다 `true`, `RESTARTS` 가 `0` 인지를 본다.
|
||||
|
||||
**`grep keycloak` 은 BFF 파드도 잡는다.** B층을 먼저 밟아 `keycloak-pattern-bff` 가 떠 있으면 그 두 줄이 같이 나온다(2026-09-17, observed). 이 편이 보려는 것은 Keycloak 파드 둘이므로 라벨로 거르는 편이 낫다.
|
||||
|
||||
```bash label="[lab host] ②b 라벨로 걸러 Keycloak 파드만 본다"
|
||||
kubectl -n keycloak-lab get pods -l app=keycloak \
|
||||
-o "custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount"
|
||||
```
|
||||
|
||||
**`-o` 값을 따옴표로 묶고 한 줄로 둔다.** 묶지 않으면 zsh 가 `[0]` 을 글로브로 읽어 명령이 아예 안 돌고, 중간에서 줄을 접으면 따옴표가 먼저 닫혀 뒤쪽 칸이 명령 인자로 떨어진다.
|
||||
|
||||
### 3. 마이그레이션 수 — 이 실험의 전부다
|
||||
|
||||
**무엇을 보는가** — `databasechangelog` 의 행 수. 처음 한 번은 읽는 형태로 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ① psql 이 무엇을 돌려주는지 한 번 본다"
|
||||
```bash label="[lab host] ① psql 이 무엇을 돌려주는지 한 번 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select count(*) from databasechangelog"
|
||||
```
|
||||
@@ -186,7 +197,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
비교용으로 값만 뽑는 형태도 익혀 둔다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 값만 뽑는 형태"
|
||||
```bash label="[lab host] ② 값만 뽑는 형태"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from databasechangelog"
|
||||
```
|
||||
@@ -199,7 +210,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
**이 값을 화면 밖에 적어 둔다.** 무엇이 마지막으로 적용됐는지도 한 번 본다. 나중에 「스키마가 언제 움직였나」를 물을 때 여기를 본다. 이 실험대는 개수만 셌고(observed), 아래는 가이드가 미검증으로 표시한 형태다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ③ 마지막 다섯 줄을 본다 (미검증)"
|
||||
```bash label="[lab host] ③ 마지막 다섯 줄을 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select id, author, orderexecuted, dateexecuted from databasechangelog
|
||||
order by orderexecuted desc limit 5"
|
||||
@@ -211,7 +222,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**무엇을 보는가** — 판올림이 세션을 건드리는지 판정할 재료.
|
||||
|
||||
```bash label="[kc-lab-1] 세션 수를 센다"
|
||||
```bash label="[lab host] 세션 수를 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from offline_user_session"
|
||||
```
|
||||
@@ -229,7 +240,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
**무엇을 보는가** — 멤버 수와 괄호 안의 판 번호.
|
||||
|
||||
```bash label="[kc-lab-1] 클러스터 뷰 마지막 줄을 본다"
|
||||
```bash label="[lab host] 클러스터 뷰 마지막 줄을 본다"
|
||||
kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
|
||||
```
|
||||
|
||||
@@ -245,13 +256,13 @@ kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
|
||||
|
||||
**무엇을 보는가** — 올라갈 곳이 실제로 있는가. 처음 한 번은 응답을 그대로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 응답을 통째로 본다"
|
||||
```bash label="[lab host] ① 응답을 통째로 본다"
|
||||
curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyActiveTags=true"
|
||||
```
|
||||
|
||||
한 줄짜리 JSON 이 통째로 나온다. 어떤 필드가 있는지 보고 나서 자른다. 이 실험대는 `jq` 가 없어 이렇게 읽었고(observed), 가이드가 그 줄을 미검증으로 표시했다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② 이름만 잘라 본다 (미검증)"
|
||||
```bash label="[lab host] ② 이름만 잘라 본다 (미검증)"
|
||||
curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyActiveTags=true" \
|
||||
| tr ',' '\n' | grep '"name"'
|
||||
```
|
||||
@@ -264,7 +275,7 @@ curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyA
|
||||
|
||||
① 1초 간격으로 150회, 뒤에서 돌린다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 폴링을 뒤에서 돌린다"
|
||||
```bash label="[lab host] ① 폴링을 뒤에서 돌린다"
|
||||
( for i in $(seq 1 150); do
|
||||
printf '%s ' "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 \
|
||||
https://auth.hyeonworks.com/realms/master)"
|
||||
@@ -274,7 +285,7 @@ curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyA
|
||||
|
||||
그만 재려면 `kill %1` 이다. **`&` 로 붙인 작업은 그것을 띄운 창의 것이라 `kill %1` 도 그 창에서만 듣는다.** 그래서 이 한 줄은 폴링용 창에서 치고, 아래 ② 부터 관찰 절까지는 다른 창에서 친다. 두 창 다 `kc-lab-1` 이다. 30초쯤 두고 먼저 평시를 센다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 평시 응답 코드를 센다"
|
||||
```bash label="[lab host] ② 평시 응답 코드를 센다"
|
||||
tr ' ' '\n' < /tmp/d2-avail.txt | grep -c 200
|
||||
tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
|
||||
```
|
||||
@@ -293,7 +304,7 @@ tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
|
||||
|
||||
① 시각을 남기고 태그를 바꾼다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 시각을 남기고 태그를 바꾼다"
|
||||
```bash label="[lab host] ① 시각을 남기고 태그를 바꾼다"
|
||||
date '+%H:%M:%S 태그 변경'
|
||||
kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
keycloak=quay.io/keycloak/keycloak:26.7.3
|
||||
@@ -310,7 +321,7 @@ statefulset.apps/keycloak image updated
|
||||
|
||||
② 롤아웃을 기다린다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 롤아웃을 기다린다"
|
||||
```bash label="[lab host] ② 롤아웃을 기다린다"
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=600s
|
||||
date '+%H:%M:%S 롤아웃 완료'
|
||||
```
|
||||
@@ -332,10 +343,10 @@ partitioned roll out complete: 2 new pods have been updated...
|
||||
|
||||
결과를 해석하기 전에, 주입이 의도한 것을 정확히 했는지 먼저 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 파드가 새 이미지를 돌리는가"
|
||||
kubectl -n keycloak-lab get pods -o custom-columns=\
|
||||
NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready,\
|
||||
RESTARTS:.status.containerStatuses[0].restartCount | grep keycloak
|
||||
```bash label="[lab host] ① 파드가 새 이미지를 돌리는가"
|
||||
kubectl -n keycloak-lab get pods \
|
||||
-o "custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount" \
|
||||
| grep keycloak
|
||||
```
|
||||
|
||||
실측은 이렇다(observed, `followup/01-d2-forward-upgrade.txt`).
|
||||
@@ -350,7 +361,7 @@ quay.io/keycloak/keycloak:26.7.3
|
||||
|
||||
실제로 새 파드인지는 나이로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 나이로 새 파드인지 본다"
|
||||
```bash label="[lab host] ② 나이로 새 파드인지 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||||
```
|
||||
|
||||
@@ -365,7 +376,7 @@ keycloak-1 1/1 Running 0 28s
|
||||
|
||||
버전은 파드가 자기 입으로 말하게 한다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 로그가 말하는 판을 본다"
|
||||
```bash label="[lab host] ③ 로그가 말하는 판을 본다"
|
||||
kubectl -n keycloak-lab logs keycloak-0 | grep -i 'Keycloak 26' | tail -1
|
||||
```
|
||||
|
||||
@@ -379,9 +390,26 @@ kubectl -n keycloak-lab logs keycloak-0 | grep -i 'Keycloak 26' | tail -1
|
||||
|
||||
## 관찰
|
||||
|
||||
**2026-09-17 에 세 번을 이어서 쳤다**(observed). 네 값이 이 편의 판정 전부다.
|
||||
|
||||
| 언제 | 무엇 | 파드 | `databasechangelog` | 정문 |
|
||||
|---|---|---|---|---|
|
||||
| `15:32:15` → `15:33:40` (85초) | 26.7.0 → **26.7.3** | 둘 다 `true` · `RESTARTS 0` | 210 → **210** | 200 |
|
||||
| `15:33:48` → `15:34:55` (67초) | 26.7.3 → **26.7.0** (롤백) | 둘 다 `true` · `RESTARTS 0` | **210** | 200 |
|
||||
| `15:34:55` | 26.7.0 → **26.0** (역방향) | `keycloak-1` 만 `false` · `RESTARTS 2` | **210** | **200** |
|
||||
|
||||
**스키마가 한 번도 안 움직였다.** `orderexecuted` 210 의 `26.7.0-cluster-event` 이 끝까지 마지막 줄이었다. 패치 판올림이라 롤백이 막히지 않은 것이고, 역방향이 막힌 까닭은 판올림이 남긴 스키마가 아니라 **체크섬**이다.
|
||||
|
||||
```text
|
||||
liquibase.exception.ValidationFailedException: Validation Failed:
|
||||
1 changesets check sum
|
||||
```
|
||||
|
||||
**그런데 정문은 세 번 내내 `200` 이었다.** StatefulSet 이 파드를 하나씩 갈아 끼우고 `keycloak-0` 이 26.7.0 에 남아 있었기 때문이다. **한 파드가 `CrashLoopBackOff` 인데 밖에서는 아무 일도 없어 보인다** — 이 층이 「조용한 실패」라고 부르는 것이 이 모양이다. 실패한 기동은 스키마도 안 건드렸다(210 그대로).
|
||||
|
||||
### 1. 정방향은 무중단이었는가
|
||||
|
||||
```bash label="[kc-lab-1] 폴링 결과를 센다"
|
||||
```bash label="[lab host] 폴링 결과를 센다"
|
||||
tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
|
||||
```
|
||||
|
||||
@@ -402,7 +430,7 @@ tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
|
||||
|
||||
주입 전에 친 것과 똑같은 명령이다.
|
||||
|
||||
```bash label="[kc-lab-1] 마이그레이션 수를 다시 센다"
|
||||
```bash label="[lab host] 마이그레이션 수를 다시 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from databasechangelog"
|
||||
```
|
||||
@@ -431,7 +459,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
⓪ §1 에서 센 `200 응답: 87 회 / 비200: 0` 을 먼저 손으로 적어 둔다. 루프가 `>` 로 파일을 잘라 쓰기 때문에 다시 띄우면 그 값은 화면에서 사라진다. 폴링 창에서 `jobs` 를 쳐 아직 돌고 있으면 `kill %1` 로 멈춘 뒤, 같은 창에서 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ⓪ 폴링 창에서 다시 띄운다"
|
||||
```bash label="[lab host] ⓪ 폴링 창에서 다시 띄운다"
|
||||
( for i in $(seq 1 150); do
|
||||
printf '%s ' "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 \
|
||||
https://auth.hyeonworks.com/realms/master)"
|
||||
@@ -441,7 +469,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
① 시각을 남기고 태그를 내린다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 태그를 26.7.0 으로 되돌린다"
|
||||
```bash label="[lab host] ① 태그를 26.7.0 으로 되돌린다"
|
||||
date '+%H:%M:%S 롤백'
|
||||
kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
keycloak=quay.io/keycloak/keycloak:26.7.0
|
||||
@@ -466,7 +494,7 @@ partitioned roll out complete: 2 new pods have been updated...
|
||||
|
||||
### 4. 전환 순간의 000 을 읽는다
|
||||
|
||||
```bash label="[kc-lab-1] 비200 이 어디에 있는지 본다"
|
||||
```bash label="[lab host] 비200 이 어디에 있는지 본다"
|
||||
tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
|
||||
grep -n '000' /tmp/d2-avail.txt
|
||||
```
|
||||
@@ -497,7 +525,7 @@ grep -n '000' /tmp/d2-avail.txt
|
||||
|
||||
① 시각을 남기고 26.0 으로 내린다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 26.0 으로 내린다"
|
||||
```bash label="[lab host] ① 26.0 으로 내린다"
|
||||
date '+%H:%M:%S 26.0 으로 내린다'
|
||||
kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
keycloak=quay.io/keycloak/keycloak:26.0
|
||||
@@ -505,7 +533,7 @@ kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
|
||||
② 이번에는 `rollout status` 로 기다리지 말고 눈으로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드 상태를 눈으로 따라간다"
|
||||
```bash label="[lab host] ② 파드 상태를 눈으로 따라간다"
|
||||
kubectl -n keycloak-lab get pods -w
|
||||
```
|
||||
|
||||
@@ -532,7 +560,7 @@ statefulset.apps/keycloak image updated
|
||||
|
||||
### 6. 왜 실패했는지 물어본다
|
||||
|
||||
```bash label="[kc-lab-1] ① Liquibase 관련 줄만 뽑는다"
|
||||
```bash label="[lab host] ① Liquibase 관련 줄만 뽑는다"
|
||||
kubectl -n keycloak-lab logs keycloak-1 | grep -iE 'liquibase|changeset|validation'
|
||||
```
|
||||
|
||||
@@ -547,16 +575,16 @@ kubectl -n keycloak-lab logs keycloak-1 | grep -iE 'liquibase|changeset|validati
|
||||
|
||||
`1 changesets check sum` 이고 개수가 1이다. 26.7.0 이 적용한 changeset 하나를 26.0 도 알고 있는데 정의가 다르다. 같은 changeset 이 버전 사이에 수정됐고, Liquibase 는 스키마를 반쯤 아는 상태로 서비스하느니 기동 자체를 거부한다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드가 이미 죽었으면 직전 로그를 본다"
|
||||
```bash label="[lab host] ② 파드가 이미 죽었으면 직전 로그를 본다"
|
||||
kubectl -n keycloak-lab logs keycloak-1 --previous
|
||||
```
|
||||
|
||||
### 7. 그런데 서비스는 살아 있다
|
||||
|
||||
```bash label="[kc-lab-1] 밖과 Service 와 StatefulSet 을 함께 본다"
|
||||
```bash label="[lab host] 밖과 Service 와 StatefulSet 을 함께 본다"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||||
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
|
||||
-o custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
|
||||
-o "custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready"
|
||||
kubectl -n keycloak-lab get statefulset keycloak
|
||||
```
|
||||
|
||||
@@ -587,7 +615,7 @@ A-8 에서 「무중단은 replica ≥ 2 와 readiness 의 조합」이라고
|
||||
|
||||
같은 명령을 세 번째로 친다.
|
||||
|
||||
```bash label="[kc-lab-1] 마이그레이션 수를 세 번째로 센다"
|
||||
```bash label="[lab host] 마이그레이션 수를 세 번째로 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from databasechangelog"
|
||||
```
|
||||
@@ -632,7 +660,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
① 시각을 남기고 태그를 되돌린 뒤 롤아웃을 기다린다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 태그를 되돌리고 롤아웃을 기다린다"
|
||||
```bash label="[lab host] ① 태그를 되돌리고 롤아웃을 기다린다"
|
||||
date '+%H:%M:%S 복귀'
|
||||
kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
keycloak=quay.io/keycloak/keycloak:26.7.0
|
||||
@@ -686,7 +714,7 @@ keycloak-1 1/1 Running 0 28s
|
||||
|
||||
**업그레이드 전 행 수를 안 적었을 때**는 덤프 안에 그 테이블이 통째로 들어 있다. 가이드가 미검증으로 표시한 줄이다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] 덤프에서 행 수를 되찾는다 (미검증)"
|
||||
```bash label="[lab host] 덤프에서 행 수를 되찾는다 (미검증)"
|
||||
sed -n '/^COPY public.databasechangelog /,/^\\\.$/p' /tmp/pre-upgrade.sql | wc -l
|
||||
```
|
||||
|
||||
|
||||
+63
-23
@@ -41,9 +41,24 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 `kc-lab-1` 에서 친다. k3s 서버의 저장 파일도 이 노드에 있어서 디스크를 보는 경로를 여기서 칠 수 있다. 게스트(`kc-lab-1`/`kc-lab-2`)의 `sudo` 는 무암호이고, 호스트와 다르다.
|
||||
**기계가 둘이다.** `kubectl` 은 `[lab host]` 에서 치고, k3s 서버의 저장 파일을 들여다보는 `k3s`·`ls`·`grep`·`strings` 는 그 파일이 있는 `[kc-lab-1]` 에서 친다. 게스트(`kc-lab-1`/`kc-lab-2`)의 `sudo` 는 무암호이고, 호스트와 다르다.
|
||||
|
||||
가이드의 전제는 「`kubectl` 은 `sudo` 로 쓴다」인데 본문의 `kubectl` 줄에는 `sudo` 가 없고 `k3s`·`ls`·`grep` 에만 붙어 있다. 아래는 본문의 형태를 그대로 옮긴다. **먼저 `sudo` 없이 치고, 권한 때문에 막히면 그때 앞에 `sudo` 를 붙인다.** 어느 쪽인지는 kubeconfig 를 어디에 뒀나가 가른다 — D-1 은 같은 전제를 옮기면서 「kubeconfig 를 사용자 홈에 복사해 뒀다면 `sudo` 는 빼도 된다」를 괄호로 달아 두었고, D-3 은 그 괄호가 없다.
|
||||
**`[kc-lab-1]` 라벨이 붙은 블록은 게스트 셸이다.** lab host 에서 `ssh kc-lab-1` 로 들어가서 치고, 끝나면 `exit` 로 나온다. `ssh kc-lab-2 '…'` 한 줄 형태는 lab host 에서 그대로 쳐도 되므로 `[lab host]` 로 두었다.
|
||||
|
||||
원 가이드는 둘을 한 기계로 묶어 전부 `kc-lab-1` 에서 치라고 적었고, 전제 한 줄은 「`kubectl` 은 `sudo` 로 쓴다」인데 본문의 `kubectl` 줄에는 `sudo` 가 없다. 그 조합은 이 실험대에서 안 돈다 — 2026-09-17 에 양쪽에서 쳐서 확인했다(observed).
|
||||
|
||||
| 어디서 | `kubectl …` | `sudo kubectl …` |
|
||||
|---|---|---|
|
||||
| lab host | 된다 | 안 된다 |
|
||||
| `kc-lab-1` | 안 된다 | 된다 |
|
||||
|
||||
기반 가이드가 kubeconfig 를 lab host 의 `~/.kube/config` 에만 두고 게스트의 사용자 홈에는 일부러 두지 않기 때문이다. `kc-lab-1` 에서 `sudo` 없이 치면 이렇게 끝난다.
|
||||
|
||||
```text
|
||||
error: error loading config file "/etc/rancher/k3s/k3s.yaml": open /etc/rancher/k3s/k3s.yaml: permission denied
|
||||
```
|
||||
|
||||
그래서 `kubectl` 은 `[lab host]` 에서 치고, 저장 파일을 보는 넷만 `[kc-lab-1]` 에서 `sudo` 로 친다. 굳이 `kc-lab-1` 한 기계에서 다 치겠다면 `kubectl` 에도 `sudo` 를 붙인다. D-1 은 같은 전제에 「kubeconfig 를 사용자 홈에 복사해 뒀다면 `sudo` 는 빼도 된다」를 괄호로 달아 두었는데, 그쪽은 반대 방향을 말한다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -95,7 +110,7 @@ Secret 이 base64 를 쓰는 까닭은 감추려는 것이 아니라 YAML 에
|
||||
|
||||
**파괴적인 단계가 없는 편이다.** 만드는 것은 카나리아 Secret 하나뿐이고 복구 절에서 지운다. 전 구간 약 15분. 되돌리기는 한 줄이다.
|
||||
|
||||
```bash label="[kc-lab-1] 되돌리기 한 줄"
|
||||
```bash label="[lab host] 되돌리기 한 줄"
|
||||
kubectl -n keycloak-lab delete secret d3-canary
|
||||
```
|
||||
|
||||
@@ -111,7 +126,7 @@ Secret 목록 → describe 가 감추는 화면 → 키 이름만 → 길이만
|
||||
|
||||
**무엇을 보는가** — 이름과 키 개수.
|
||||
|
||||
```bash label="[kc-lab-1] Secret 목록을 본다"
|
||||
```bash label="[lab host] Secret 목록을 본다"
|
||||
kubectl -n keycloak-lab get secret
|
||||
```
|
||||
|
||||
@@ -129,7 +144,7 @@ kubectl -n keycloak-lab get secret
|
||||
|
||||
**무엇을 보는가** — 키 이름과 바이트 수.
|
||||
|
||||
```bash label="[kc-lab-1] describe 화면을 본다"
|
||||
```bash label="[lab host] describe 화면을 본다"
|
||||
kubectl -n keycloak-lab describe secret bff-secrets
|
||||
```
|
||||
|
||||
@@ -149,7 +164,7 @@ kubectl -n keycloak-lab describe secret bff-secrets
|
||||
|
||||
**무엇을 보는가** — 어떤 키가 들어 있는가. 값은 보지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 키 이름만 뽑는다"
|
||||
```bash label="[lab host] ① 키 이름만 뽑는다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets -o jsonpath='{.data}' \
|
||||
| tr ',' '\n' | grep -o '"[A-Z_]*"'
|
||||
```
|
||||
@@ -163,7 +178,7 @@ kubectl -n keycloak-lab get secret keycloak-lab-secrets -o jsonpath='{.data}' \
|
||||
|
||||
`jq` 가 없어서 `tr` 과 `grep` 으로 자른다. D-2 가 레지스트리 태그 목록을 자를 때 쓴 것과 같은 수법이고, `jq` 가 없다는 전제가 여기서도 형태를 정한다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 값 대신 길이를 잰다"
|
||||
```bash label="[lab host] ② 값 대신 길이를 잰다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d | wc -c
|
||||
```
|
||||
@@ -190,7 +205,7 @@ kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
|
||||
① 카나리아를 만든다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 카나리아 Secret 을 만든다"
|
||||
```bash label="[lab host] ① 카나리아 Secret 을 만든다"
|
||||
kubectl -n keycloak-lab create secret generic d3-canary \
|
||||
--from-literal=CANARY=d3-canary-zq7v-do-not-use
|
||||
```
|
||||
@@ -207,7 +222,7 @@ secret/d3-canary created
|
||||
|
||||
## 주입 검증
|
||||
|
||||
```bash label="[kc-lab-1] 카나리아가 심겼는지 본다"
|
||||
```bash label="[lab host] 카나리아가 심겼는지 본다"
|
||||
kubectl -n keycloak-lab get secret d3-canary
|
||||
kubectl -n keycloak-lab describe secret d3-canary
|
||||
```
|
||||
@@ -232,7 +247,7 @@ CANARY: 25 bytes
|
||||
|
||||
값을 아는 카나리아로 먼저 해 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 카나리아를 API 로 뽑아 본다"
|
||||
```bash label="[lab host] ① 카나리아를 API 로 뽑아 본다"
|
||||
kubectl -n keycloak-lab get secret d3-canary \
|
||||
-o jsonpath='{.data.CANARY}' | base64 -d; echo
|
||||
```
|
||||
@@ -306,7 +321,14 @@ sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db
|
||||
sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db-wal
|
||||
```
|
||||
|
||||
이 실험대는 카나리아 대신 실제 값으로 쟀다(observed). 위의 카나리아 형태는 가이드가 미검증으로 표시했다(unknown). 실제로 나온 결과가 이것이다(observed, `02-at-rest.txt`).
|
||||
이 실험대는 카나리아 대신 실제 값으로 쟀다(observed). **위의 카나리아 형태는 2026-09-17 에 쳐서 확인했다**(observed) — 그때까지 가이드가 미검증으로 표시해 둔 줄이다.
|
||||
|
||||
```text
|
||||
state.db 0
|
||||
state.db-wal 1
|
||||
```
|
||||
|
||||
**그 `0` 과 `1` 이 가이드가 예측만 하고 못 가른 것을 가른다.** 방금 만든 값은 아직 본체로 안 내려가고 `-wal` 에만 있다. 그러니 본체에서 `0` 이 나왔다고 「평문이 없다」가 아니라, **`-wal` 까지 봐야 답이 나온다.** 실제로 나온 결과가 이것이다(observed, `02-at-rest.txt`).
|
||||
|
||||
```text
|
||||
=== ★ 저장 파일에서 비밀번호가 그대로 보이는가 ===
|
||||
@@ -327,11 +349,19 @@ sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db
|
||||
|
||||
**가이드는 이 두 줄의 인자에 클라이언트 비밀 평문을 적어 두었다. 값은 옮기지 않는다** — 그리고 가이드 자신이 주입 절에서 「진짜 비밀번호를 `grep` 인자로 쓰면 셸 히스토리와 `ps` 에 남는다」고 적었으므로, 아래에는 카나리아 문자열을 넣었다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ -wal 과 strings 로 한 번 더 본다 (미검증)"
|
||||
```bash label="[kc-lab-1] ④ -wal 과 strings 로 한 번 더 본다"
|
||||
sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db-wal
|
||||
sudo strings /var/lib/rancher/k3s/server/db/state.db | grep -c 'd3-canary-zq7v-do-not-use'
|
||||
sudo sh -c 'strings /var/lib/rancher/k3s/server/db/state.db | grep -c d3-canary-zq7v-do-not-use'
|
||||
```
|
||||
|
||||
:::warning
|
||||
|
||||
**게스트에 `strings` 가 없다.** `binutils` 가 안 깔려 있어 그대로 치면 `sh: 1: strings: not found` 로 끝난다(2026-09-17, observed). `sudo apt install -y binutils` 로 깔거나, `strings` 없이 `grep -c` 만으로도 같은 답이 나온다 — 둘 다 쳐서 같은 수를 받았다(observed).
|
||||
|
||||
:::
|
||||
|
||||
**`sudo strings … | grep` 이 아니라 `sudo sh -c '…'` 인 까닭**은 파이프가 `sudo` 밖에서 이어지기 때문이다. 앞의 형태로 치면 `strings` 만 root 로 돌고 `grep` 은 일반 사용자로 도는데, 여기서는 읽는 쪽이 `strings` 라 결과는 같다. 다만 파일을 `grep` 이 직접 읽는 아래 형태에서는 `sudo` 가 `grep` 에 붙어야 한다.
|
||||
|
||||
| 왜 안 나올 수 있나 | 확인 |
|
||||
|---|---|
|
||||
| 아직 `-wal` 에만 있다 | `-wal` 을 같이 `grep` |
|
||||
@@ -357,7 +387,7 @@ k3s 는 `--secrets-encryption` 플래그로 켤 수 있다. 지금은 안 켜져
|
||||
|
||||
어느 파드를 볼지 먼저 정한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① BFF 파드를 본다"
|
||||
```bash label="[lab host] ① BFF 파드를 본다"
|
||||
kubectl -n keycloak-lab get pods -l app=bff
|
||||
```
|
||||
|
||||
@@ -365,7 +395,7 @@ kubectl -n keycloak-lab get pods -l app=bff
|
||||
|
||||
**치기 전에 — 이 줄은 값을 화면에 찍는다.** 나오는 것은 카나리아가 아니라 `KEYCLOAK_CLIENT_SECRET` 과 `BFF_DB_PASSWORD` 의 평문이다. 바로 아래 실측을 `<평문 14자>` 로 가린 것은 이 문서이지 당신의 터미널이 아니다. 찍힌 값은 스크롤백과 셸 히스토리에 남고, 화면을 공유 중이면 보는 사람 모두에게 간다. 이 편이 「읽기 전에」에서 세운 「남의 진짜 비밀은 길이와 키 이름까지만 본다」를 이 줄 하나가 벗어나는데, 같은 결론을 값 없이 내는 명령은 가이드에 없다(unknown). 운영 클러스터에서는 치지 않는다. 실험대에서 쳤으면 복구 절의 `history` 확인까지 마치고, 운영 값을 찍었으면 회전(B-6·B-7)으로 이어 간다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드 안의 환경변수를 본다 (미검증 · 평문이 화면에 찍힌다)"
|
||||
```bash label="[lab host] ② 파드 안의 환경변수를 본다 (미검증 · 평문이 화면에 찍힌다)"
|
||||
kubectl -n keycloak-lab exec deploy/bff -- sh -c 'env | grep -iE "secret|password"'
|
||||
```
|
||||
|
||||
@@ -380,7 +410,7 @@ kubectl -n keycloak-lab exec deploy/bff -- sh -c 'env | grep -iE "secret|passwor
|
||||
|
||||
`exec` 이 `deploy/bff` 로 안 되면(파드가 종료 중이거나 여럿이면) 이름을 골라 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ Running 인 파드 이름을 고른다"
|
||||
```bash label="[lab host] ③ Running 인 파드 이름을 고른다"
|
||||
kubectl -n keycloak-lab get pod -l app=bff \
|
||||
--field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}'; echo
|
||||
```
|
||||
@@ -391,7 +421,7 @@ kubectl -n keycloak-lab get pod -l app=bff \
|
||||
|
||||
**이 줄도 값을 화면에 찍는다.** ②에서 본 것과 같은 평문이 같은 자국을 남긴다. 여기서 확인하려는 것은 「같은 값이 또 나오는가」뿐이므로, 화면을 공유 중이거나 운영 클러스터에 붙어 있으면 치지 않고 ②의 결과로 판정한다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 다른 프로세스의 환경변수를 읽는다 (미검증 · 평문이 화면에 찍힌다)"
|
||||
```bash label="[lab host] ④ 다른 프로세스의 환경변수를 읽는다 (미검증 · 평문이 화면에 찍힌다)"
|
||||
kubectl -n keycloak-lab exec deploy/bff -- \
|
||||
sh -c 'tr "\0" "\n" < /proc/1/environ | grep -i secret'
|
||||
```
|
||||
@@ -418,7 +448,7 @@ volumeMounts:
|
||||
|
||||
### 4. ④ RBAC — 유일하게 막는다
|
||||
|
||||
```bash label="[kc-lab-1] ① 기본 서비스계정이 Secret 을 읽을 수 있는지 묻는다"
|
||||
```bash label="[lab host] ① 기본 서비스계정이 Secret 을 읽을 수 있는지 묻는다"
|
||||
kubectl auth can-i get secrets -n keycloak-lab \
|
||||
--as=system:serviceaccount:keycloak-lab:default
|
||||
```
|
||||
@@ -433,7 +463,7 @@ kubectl auth can-i get secrets -n keycloak-lab \
|
||||
|
||||
어떤 권한이 있는지 통째로 보는 형태도 가이드에 있고, 미검증이다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② 권한 목록과 Role 을 본다 (미검증)"
|
||||
```bash label="[lab host] ② 권한 목록과 Role 을 본다 (미검증)"
|
||||
kubectl auth can-i --list -n keycloak-lab \
|
||||
--as=system:serviceaccount:keycloak-lab:default
|
||||
kubectl -n keycloak-lab get role,rolebinding
|
||||
@@ -473,7 +503,7 @@ kubectl -n keycloak-lab get role,rolebinding
|
||||
|
||||
① 지우고 목록을 다시 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 카나리아를 지우고 목록을 본다"
|
||||
```bash label="[lab host] ① 카나리아를 지우고 목록을 본다"
|
||||
kubectl -n keycloak-lab delete secret d3-canary
|
||||
kubectl -n keycloak-lab get secret
|
||||
```
|
||||
@@ -492,13 +522,23 @@ secret "d3-canary" deleted
|
||||
|
||||
### 2. 지웠다고 파일에서 없어지지는 않는다
|
||||
|
||||
아래는 미검증이고, 이 실험은 삭제 후를 재지 않았다(unknown).
|
||||
**2026-09-17 에 삭제 후를 쟀다**(observed). 그때까지 이 실험이 안 밟고 넘어간 단계다.
|
||||
|
||||
```bash label="[kc-lab-1] 삭제 뒤에 저장 파일을 다시 본다 (미검증)"
|
||||
```bash label="[kc-lab-1] 삭제 뒤에 저장 파일을 다시 본다"
|
||||
sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db
|
||||
sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db-wal
|
||||
```
|
||||
|
||||
**지운 뒤에 오히려 늘었다.**
|
||||
|
||||
```text
|
||||
state.db state.db-wal
|
||||
삭제 전 0 1
|
||||
삭제 뒤 0 7
|
||||
```
|
||||
|
||||
`kubectl delete secret` 은 API 에서 그 객체를 없앨 뿐이고, **그 삭제 자체가 같은 값을 담은 레코드를 `-wal` 에 더 쓴다.** 그래서 「Secret 을 지웠다」와 「그 값이 디스크에서 사라졌다」는 다른 사건인 정도가 아니라, 지우는 동작이 흔적을 **늘린다.**
|
||||
|
||||
`0` 이 나오면 「이 시점에 이 형태로는 안 보인다」이고, `0` 이 아니면 지운 Secret 의 평문이 아직 파일에 있는 것이다. 어느 쪽이든 관찰 절의 결론은 안 바뀐다 — 판정은 이미 `2` 에서 났다.
|
||||
|
||||
데이터베이스 파일은 지운 행의 공간을 즉시 0으로 덮어쓰지 않는다. 「Secret 을 지웠다」와 「그 값이 디스크에서 사라졌다」는 다른 사건이고, 비밀이 유출됐을 때 실제로 해야 하는 일이 삭제가 아니라 회전(rotation)인 까닭이 여기 있다 — B-6·B-7 의 주제다.
|
||||
@@ -508,7 +548,7 @@ sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db
|
||||
| 항목 | 명령 | 돌아왔을 때 |
|
||||
|---|---|---|
|
||||
| 카나리아 | `kubectl -n keycloak-lab get secret d3-canary` | `NotFound` |
|
||||
| Secret 목록 | `kubectl -n keycloak-lab get secret` | 세 개 |
|
||||
| Secret 목록 | `kubectl -n keycloak-lab get secret` | 카나리아를 뺀 나머지. 2026-09-17 의 이 실험대에는 `keycloak-lab-secrets` 하나뿐이었다(observed) |
|
||||
| 파드 | `kubectl -n keycloak-lab get pods` | 전부 `Running` (아무것도 안 건드렸다) |
|
||||
| 터미널 | `history \| tail -40` | 비밀번호가 찍힌 줄이 어디까지 남았는지 본다 |
|
||||
|
||||
|
||||
+43
-5
@@ -212,6 +212,18 @@ Fri 2026-09-04 17:03:46 KST 1h 54min Fri 2026-09-04 03:19:39 KST 11h ago
|
||||
|
||||
`NEXT`/`LEFT` 가 채워져 있는가, `LAST`/`PASSED` 가 하루 안쪽인가를 본다. 표가 통째로 비면 타이머가 없는 것이고, 이름이 배포판마다 다르므로 `systemctl list-timers --all | grep -i certbot` 으로 찾는다.
|
||||
|
||||
**기계 이름과 유닛 이름이 둘 다 이 실험대와 다르다.** 2026-09-17 에 쳐서 확인했다(observed).
|
||||
|
||||
첫째, 이 편은 `ssh test-server` 로 간다고 적는데 **certbot 은 엣지 게스트에 있다.** 기반 가이드 03 이 엣지 nginx 를 호스트에서 `kc-lab-edge` 로 옮겼고 04 가 certbot 을 거기 깔았기 때문이다. `test-server` 에도 `certbot` 실행 파일은 있지만 타이머가 없다.
|
||||
|
||||
```text
|
||||
[test-server] systemctl list-timers certbot-renew.timer → 0 timers listed.
|
||||
[kc-lab-edge] systemctl list-timers --all | grep -i certbot
|
||||
Thu 2026-09-17 17:06:05 UTC 10h left certbot.timer certbot.service
|
||||
```
|
||||
|
||||
둘째, Debian 12 의 유닛 이름은 **`certbot.timer` · `certbot.service`** 이고 `certbot-renew.*` 가 아니다. 그래서 이 편의 `systemctl list-timers certbot-renew.timer` 와 `systemctl cat certbot-renew.service` 는 이 실험대에서 **빈 표와 오류**를 낸다. 아래부터는 `ssh kc-lab-edge` 로 가고 유닛 이름은 `--all | grep -i certbot` 이 찾아 준 것을 쓴다.
|
||||
|
||||
```bash label="[test-server] ② 서비스 상태와 오늘 journal 을 본다"
|
||||
ssh test-server 'systemctl status certbot-renew.service'
|
||||
```
|
||||
@@ -250,15 +262,33 @@ Let's Encrypt 는 90일 발급이고 certbot 은 30일 남았을 때 갱신한
|
||||
|
||||
**무엇을 보는가** — 갱신된 인증서를 서버에 읽히는 경로는 셋뿐이고, 셋을 하나씩 연다. 아직 아무것도 주입하지 않았는데 이 실험의 원인 진단이 여기서 이미 끝난다.
|
||||
|
||||
```bash label="[test-server] ① 유닛 본문을 본다"
|
||||
ssh test-server 'systemctl cat certbot-renew.service'
|
||||
```bash label="[kc-lab-edge] ① 유닛 본문을 본다"
|
||||
ssh kc-lab-edge 'systemctl cat certbot.service'
|
||||
```
|
||||
|
||||
```bash label="[test-server] ② 타이머 본문을 본다"
|
||||
ssh test-server 'systemctl cat certbot-renew.timer'
|
||||
```bash label="[kc-lab-edge] ② 타이머 본문을 본다"
|
||||
ssh kc-lab-edge 'systemctl cat certbot.timer'
|
||||
```
|
||||
|
||||
**어디를 보나** — 실측은 이렇다(observed, `07-renewal-hook-missing.txt`).
|
||||
```bash label="[kc-lab-edge] ③ 배포 훅 디렉터리가 비었는지 본다"
|
||||
ssh kc-lab-edge 'sudo ls -l /etc/letsencrypt/renewal-hooks/deploy/'
|
||||
```
|
||||
|
||||
**2026-09-17 실측**(observed) — 이름은 다르지만 **이 편이 찾는 것은 그대로 없다.**
|
||||
|
||||
```text
|
||||
# /lib/systemd/system/certbot.service
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/certbot -q renew --no-random-sleep-on-renew
|
||||
PrivateTmp=true
|
||||
|
||||
/etc/letsencrypt/renewal-hooks/deploy/ total 0
|
||||
```
|
||||
|
||||
`ExecStartPost` 도 `--deploy-hook` 도 없고 훅 디렉터리도 비어 있다. **갱신은 돌지만 받은 것을 누가 읽게 만드는 일은 아무도 하지 않는다** — 이 편의 결론이 유닛 이름과 무관하게 성립한다는 뜻이다.
|
||||
|
||||
**어디를 보나** — 원래 실행의 실측은 이렇다(observed, `07-renewal-hook-missing.txt`).
|
||||
|
||||
```text
|
||||
# /usr/lib/systemd/system/certbot-renew.service
|
||||
@@ -1072,6 +1102,14 @@ crt.sh 에 관한 곁다리도 실측이다. 발급 사실은 Certificate Transp
|
||||
|
||||
인증서에 SCT 가 박혀 있다는 것과 crt.sh 가 그것을 색인했다는 것은 다르다. 관측 도구가 진실의 부분집합만 본다는, A-2 의 `up` 지표와 같은 종류의 함정이다.
|
||||
|
||||
## 이 실험대에서 아직 못 밟은 단계
|
||||
|
||||
2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.**
|
||||
|
||||
- **남은 것** — 인증서 발급과 갱신·reload 판정 전부. Cloudflare API 토큰이 있어야 DNS-01 이 돈다. 지금까지 밟은 것 — 기계·유닛 이름 대조, 기본 유닛에 훅이 없다는 확인.
|
||||
- **막는 것** — `https://auth.hyeonworks.com` 이 서지 않는다. 와일드카드 인증서(Cloudflare API 토큰이 필요한 DNS-01)와, 밖에서 실험대에 닿는 길(호스트의 libvirt `guest_input` 구멍 — A-4 에서 확인한 `ExecStartPost` 누락)이 둘 다 있어야 한다.
|
||||
- **그때까지 이 편의 실측 가운데 `(observed)` 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다.** 나머지는 원래 실행의 값이다.
|
||||
|
||||
## 무엇이 관측이고 무엇이 아닌가
|
||||
|
||||
- (observed) 주입 전 인증서의 `subject`·`issuer`·`notBefore=Sep 3 00:47:23 2026 GMT`·`notAfter=Dec 2 00:47:22 2026 GMT` 와 SAN 세 이름, SCT 두 줄, 체인 네 줄과 `Verify return code: 0 (ok)`, 타이머 표와 `status=0/SUCCESS`, `남은 일수: 88일`, 유닛 본문과 `ExecStart=/usr/bin/certbot -q renew`, 훅 디렉터리의 `Permission denied` 와 `sudo` 로 본 `total 8` 셋, `Discovered plugins: dns-cloudflare, manual, null, standalone, webroot` 와 `certbot 5.7.0`, 워커 두 줄(`585`·`586`·`80529`), 시계 측정 네 줄과 `+106.1` 세 번, 대조군 900건, 전송 중 대조군의 `845361`·`41.392198s`, `sudo -n -l` 두 줄, `Serial Number: 6c7cb6df1da8a6d7995d93c264bb9ecea1d`, `archive/` 여덟 줄과 `2026-09-04 17:22:13` mtime, 감시의 `serial=0520BB6416D569E26697B1691440F523B853` 과 161표본, `ssl_certificate` 두 줄, 일련번호가 바뀐 `08:58:52` 와 그 앞 구간의 `428회`, 폴링 `8856건` 과 구간 셋의 중앙·p95·최대, 전송 중 세 줄과 `845361`·`연결수=1`·`curl종료=0`, 아티팩트 76건과 `50µs`·`0/100`, reload 뒤의 워커 `28829`, crt.sh 의 `[]` 와 13건.
|
||||
|
||||
+40
-3
@@ -43,7 +43,17 @@ certbot 의 `deploy/` 훅에 두 줄짜리 파일 하나를 넣고, 갱신 뒤 n
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
기계가 둘이다. 밖에서 보는 `openssl` 은 `[dev]` 에서 치고, 주입은 전부 `[test-server]` 쪽이라 사람이 비밀번호를 친다.
|
||||
기계가 둘이다. 밖에서 보는 `openssl` 은 `[dev]` 에서 치고, 주입은 전부 엣지 쪽이라 사람이 비밀번호를 친다.
|
||||
|
||||
**주입하는 기계는 `test-server` 가 아니라 `kc-lab-edge` 다.** 기반 가이드 03 이 엣지 nginx 를 호스트에서 게스트로 옮겼고 04 가 거기 certbot 을 깔았다. 2026-09-17 에 양쪽에서 확인했다(observed).
|
||||
|
||||
```text
|
||||
[test-server] ps -ef | grep '[n]ginx' → nginx 프로세스가 없다
|
||||
[kc-lab-edge] root 1065 nginx: master process /usr/sbin/nginx …
|
||||
www-data 1106 nginx: worker process
|
||||
```
|
||||
|
||||
그래서 아래 `[test-server]` 라벨이 붙은 블록은 전부 `ssh kc-lab-edge` 로 간다. 그 게스트의 `sudo` 는 무암호라 사람이 비밀번호를 칠 일도 없다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -154,7 +164,15 @@ echo | openssl s_client -connect auth.hyeonworks.com:443 -servername auth.hyeonw
|
||||
ssh -t test-server 'sudo ls -la /etc/letsencrypt/renewal-hooks/deploy/'
|
||||
```
|
||||
|
||||
**어디를 보나** — 실측은 이렇다(observed, `d4-certificate-renewal/12-certbot-state.txt`).
|
||||
**어디를 보나** — 2026-09-17 에 엣지에서 친 것이다(observed). 디렉터리는 있고 안은 비었다.
|
||||
|
||||
```text
|
||||
total 8
|
||||
drwxr-xr-x 2 root root 4096 Sep 17 04:33 .
|
||||
drwxr-xr-x 5 root root 4096 Sep 17 04:33 ..
|
||||
```
|
||||
|
||||
원래 실측은 이렇다(observed, `d4-certificate-renewal/12-certbot-state.txt`).
|
||||
|
||||
```text
|
||||
/etc/letsencrypt/renewal-hooks/deploy/:
|
||||
@@ -173,11 +191,22 @@ drwxr-xr-x 5 root root 4096 2026-09-03 10:46:54.658520760 +0900 ..
|
||||
|
||||
```bash label="[dev] ① 세 번 잰다"
|
||||
for i in 1 2 3; do
|
||||
A=$(date -u +%s.%N); B=$(ssh test-server 'date -u +%s.%N'); C=$(date -u +%s.%N)
|
||||
A=$(date -u +%s.%N); B=$(ssh kc-lab-edge 'date -u +%s.%N'); C=$(date -u +%s.%N)
|
||||
echo "A=$A B=$B C=$C"
|
||||
done
|
||||
```
|
||||
|
||||
**재는 대상이 엣지 게스트다.** 이 절차의 판정은 엣지 nginx 가 언제 reload 됐나이므로, 어긋난 시계를 재야 할 짝은 「보는 기계 ↔ 엣지」다.
|
||||
|
||||
2026-09-17 에 lab host 와 엣지를 나란히 찍어 보니 **엣지가 94초 느렸다**(observed).
|
||||
|
||||
```text
|
||||
lab host 2026-09-17T06:51:28.799Z
|
||||
kc-lab-edge 2026-09-17T06:49:54.782Z
|
||||
```
|
||||
|
||||
문서가 적은 `106초` 는 그때 그 짝의 값이고, **수를 옮겨 쓰지 말고 지금 자기 실험대에서 다시 잰다.** 게스트는 호스트와 따로 시계를 맞추므로 새로 만들 때마다 값이 다르다.
|
||||
|
||||
세 줄 각각에서 `B` 와 `(A+C)/2` 의 차이를 눈으로 뺀다. 그리고 세 번의 값이 서로 비슷한가를 본다 — 흔들리면 네트워크 지연이 섞였고, 안정적이면 진짜 왜곡이다.
|
||||
|
||||
**2.** 어느 쪽이 맞는지는 외부 기준으로 가른다.
|
||||
@@ -617,6 +646,14 @@ echo | openssl s_client -connect auth.hyeonworks.com:443 -servername auth.hyeonw
|
||||
|
||||
이 편이 남기는 한 문장은 「처방을 적었으면 시험한다」이다. D-4 는 원인을 정확히 셋으로 특정하고 고치는 법까지 적었고, 그 처방이 듣는지 확인하는 데 든 비용은 파일 하나와 명령 두 줄이었다. 확인하지 않은 채로 문서에 남았다면 「고치는 법」 항목은 다음 갱신일까지 아무도 시험하지 않은 문장으로 남았을 텐데, 그날이 바로 시험할 수 없는 날이다.
|
||||
|
||||
## 이 실험대에서 아직 못 밟은 단계
|
||||
|
||||
2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.**
|
||||
|
||||
- **남은 것** — 훅을 설치하고 강제 갱신으로 워커 PID 가 바뀌는지 보는 구간 전부. 인증서가 있어야 갱신할 것이 생긴다. 지금까지 밟은 것 — 훅 디렉터리가 빈 것, 두 기계 시계 차 94초.
|
||||
- **막는 것** — `https://auth.hyeonworks.com` 이 서지 않는다. 와일드카드 인증서(Cloudflare API 토큰이 필요한 DNS-01)와, 밖에서 실험대에 닿는 길(호스트의 libvirt `guest_input` 구멍 — A-4 에서 확인한 `ExecStartPost` 누락)이 둘 다 있어야 한다.
|
||||
- **그때까지 이 편의 실측 가운데 `(observed)` 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다.** 나머지는 원래 실행의 값이다.
|
||||
|
||||
## 무엇이 관측이고 무엇이 아닌가
|
||||
|
||||
- (observed) 주입 전 워커 두 줄(`585` 와 `28829`, `lstart Fri Sep 4 18:00:35`), 훅 디렉터리의 `total 8`, 시계 측정 네 줄과 `+106.1` 세 번, certbot 출력 전문(`ran with error output` · `types_hash` 경고 두 줄 · `test is successful` · `signal process started` · `Congratulations, all renewals succeeded` · `fullchain.pem (success)`), 주입 뒤 워커 두 줄(`585` · `37252` · `etimes 74` · `lstart Fri Sep 4 21:29:36 2026`), 새 인증서의 `serial=06F3E0EF4D1BB03DE58130EAAD1176101373` · `notBefore=Sep 4 11:29:18 2026 GMT` · `notAfter=Dec 3 11:29:17 2026 GMT` 와 SAN 세 이름, SCT 두 줄(`Sep 4 12:27:49.054` · `Sep 4 12:27:49.048`), `notBefore` ↔ SCT 표의 `약 89.8초` · `약 88.9초`.
|
||||
|
||||
+100
-44
@@ -42,16 +42,31 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 전부 `[kc-lab-1]` 에서 `kubectl` 과 `psql` 로 친다. 노드 자체를 건드리는 명령이 없어서 `kc-lab-2` 로 들어갈 일이 없다. `kubectl` 에 `sudo` 를 붙이지 않는다.
|
||||
명령은 전부 `[lab host]` 에서 `kubectl` 과 `psql` 로 친다. 노드 자체를 건드리는 명령이 없어서 게스트에 들어갈 일이 없다. `kubectl` 에 `sudo` 를 붙이지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] sudo 를 붙이는 쪽이 틀린 형태다"
|
||||
```bash label="[lab host] sudo 를 붙이는 쪽이 틀린 형태다"
|
||||
kubectl -n keycloak-lab get pods # 이렇게
|
||||
sudo kubectl -n keycloak-lab get pods # 이렇게 치면 안 된다
|
||||
```
|
||||
|
||||
`sudo` 를 붙이면 root 환경으로 돌아 사용자 홈의 kubeconfig 를 못 본다. root 홈에는 `~/.kube/config` 가 없어서 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝난다. 막힌 곳은 클러스터가 아니라 `kubectl` 이 어느 설정 파일을 읽느냐다.
|
||||
|
||||
터미널은 둘을 연다. 하나는 탐침 파드 셸용이라 붙잡혀 있고, 하나는 관찰용이다. 그래서 `[kc-lab-1]` 라벨이 붙은 블록이 `[탐침 파드]` 블록 사이에 끼어 있으면 **관찰용 터미널에서 친다** — 파드 셸을 나가라는 뜻이 아니다. 나가라고 할 때는 `exit` 를 블록으로 따로 적는다.
|
||||
**`kc-lab-1` 에서 치지 않는다.** 원본 가이드는 `kc-lab-1` 에서 치라고 적었는데, 기반 가이드가 세운 실험대에서는 **그 기계에 kubeconfig 가 아예 없다.** 2026-09-17 에 양쪽에서 쳐서 확인했다(observed).
|
||||
|
||||
| 어디서 | `kubectl …` | `sudo kubectl …` |
|
||||
|---|---|---|
|
||||
| lab host | 된다 | 안 된다 |
|
||||
| `kc-lab-1` | 안 된다 | 된다 |
|
||||
|
||||
`kc-lab-1` 에서 `sudo` 없이 치면 `connection refused` 가 아니라 이렇게 끝난다.
|
||||
|
||||
```text
|
||||
error: error loading config file "/etc/rancher/k3s/k3s.yaml": open /etc/rancher/k3s/k3s.yaml: permission denied
|
||||
```
|
||||
|
||||
k3s 의 `kubectl` 은 `~/.kube/config` 가 없으면 `/etc/rancher/k3s/k3s.yaml` 로 떨어지는데 그 파일은 `600 root` 다. 기반 가이드는 그 파일을 **lab host 의 `~/.kube/config` 로만** 복사하고 게스트의 사용자 홈에는 일부러 두지 않는다 — 워커 한 대가 털리면 클러스터가 통째로 털리는 구성을 피하려고 그렇게 했다. 그래서 「`sudo` 를 붙이지 않는다」는 규칙은 **lab host 의 규칙**이고, 기계를 `kc-lab-1` 로 읽으면 첫 명령부터 막힌다.
|
||||
|
||||
터미널은 둘을 연다. 하나는 탐침 파드 셸용이라 붙잡혀 있고, 하나는 관찰용이다. 그래서 `[lab host]` 라벨이 붙은 블록이 `[탐침 파드]` 블록 사이에 끼어 있으면 **관찰용 터미널에서 친다** — 파드 셸을 나가라는 뜻이 아니다. 나가라고 할 때는 `exit` 를 블록으로 따로 적는다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -96,7 +111,7 @@ sudo kubectl -n keycloak-lab get pods # 이렇게 치면 안 된다
|
||||
|
||||
**이건 상태를 부수는 실험이다.** 세션 테이블을 비우고, StatefulSet 을 재시작하고, PostgreSQL 의 문장 로깅을 켠다. **실험대에서만 한다.** 지운 세션은 돌아오지 않는다. 되돌릴 수 있는 것은 문장 로깅 하나이고, 켜기 전에 끄는 명령을 먼저 읽어 둔다.
|
||||
|
||||
```bash label="[kc-lab-1] 중간에 그만둘 때 치는 한 묶음"
|
||||
```bash label="[lab host] 중간에 그만둘 때 치는 한 묶음"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "alter system reset log_statement" -c "alter system reset log_line_prefix" \
|
||||
-c "select pg_reload_conf()"
|
||||
@@ -114,11 +129,11 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**무엇을 보는가** — 파드가 둘 다 Ready 이고 다른 기계에 나뉘어 있는지.
|
||||
|
||||
```bash label="[kc-lab-1] ① 노드를 본다"
|
||||
```bash label="[lab host] ① 노드를 본다"
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드가 어느 노드에 있는지 본다"
|
||||
```bash label="[lab host] ② 파드가 어느 노드에 있는지 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
|
||||
@@ -130,7 +145,7 @@ kubectl -n keycloak-lab get pods -o wide
|
||||
|
||||
**무엇을 보는가** — 뒤의 모든 요청이 향할 주소.
|
||||
|
||||
```bash label="[kc-lab-1] 파드 IP 를 변수에 담고 눈으로 확인한다"
|
||||
```bash label="[lab host] 파드 IP 를 변수에 담고 눈으로 확인한다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
echo "$K0 $K1"
|
||||
@@ -150,7 +165,7 @@ echo "$K0 $K1"
|
||||
|
||||
**무엇을 보는가** — 두 노드가 서로를 멤버로 세고 있는지.
|
||||
|
||||
```bash label="[kc-lab-1] 양쪽 로그에서 마지막 클러스터 뷰 한 줄씩"
|
||||
```bash label="[lab host] 양쪽 로그에서 마지막 클러스터 뷰 한 줄씩"
|
||||
kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
|
||||
kubectl -n keycloak-lab logs keycloak-1 | grep ISPN000094 | tail -1
|
||||
```
|
||||
@@ -176,7 +191,7 @@ kubectl -n keycloak-lab logs keycloak-1 | grep ISPN000094 | tail -1
|
||||
|
||||
**무엇을 보는가** — 노드가 서로를 찾는 길인 `JGROUPS_PING` 테이블에 무엇이 등록돼 있는지.
|
||||
|
||||
```bash label="[kc-lab-1] 디스커버리 테이블 세 열"
|
||||
```bash label="[lab host] 디스커버리 테이블 세 열"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select name, ip, coord from jgroups_ping order by name"
|
||||
```
|
||||
@@ -197,11 +212,11 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**무엇을 보는가** — 온라인 세션이 어느 테이블에 들어가는지.
|
||||
|
||||
```bash label="[kc-lab-1] 테이블 목록"
|
||||
```bash label="[lab host] 테이블 목록"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c "\dt"
|
||||
```
|
||||
|
||||
**어디를 보나** — `USER_SESSION` 이라는 이름이 목록에 없는 것. 실측은 이렇다(observed).
|
||||
**어디를 보나** — `USER_SESSION` 이라는 이름이 목록에 없는 것. 실측은 이렇다 — **`(100 rows)` 가운데 이 실험이 쓰는 여섯 줄만 옮겼다**(observed, 2026-09-17 에 다시 세운 실험대에서도 100행이었다).
|
||||
|
||||
```text
|
||||
public | auth_session | table | keycloak
|
||||
@@ -214,7 +229,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c
|
||||
|
||||
**이 값이 뜻하는 것** — `persistent-user-sessions`(Keycloak 26 기본값)는 새 테이블을 만들지 않고 기존 오프라인 세션 테이블을 재사용한다. `offline_flag` 컬럼으로 구분하고 `'0'` 이 일반 로그인, `'1'` 이 `offline_access` 다. 기본키가 `(user_session_id, offline_flag)` 복합키인 까닭이 여기 있고, 이 절차의 모든 질의는 `offline_flag='0'` 이다.
|
||||
|
||||
```bash label="[kc-lab-1] 지금 몇 건인지 센다"
|
||||
```bash label="[lab host] 지금 몇 건인지 센다"
|
||||
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"
|
||||
```
|
||||
@@ -225,14 +240,14 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
Keycloak 컨테이너에는 `curl` 도 `wget` 도 없어 `exec` 로 물으면 `exit 127` 이 난다. Prometheus 가 15초마다 이미 긁고 있으므로 밖에서 묻는 쪽이 짧다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 한 줄짜리 JSON 을 통째로 본다"
|
||||
```bash label="[lab host] ① 한 줄짜리 JSON 을 통째로 본다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_statistics_approximate_entries_unique'
|
||||
```
|
||||
|
||||
처음 한 번은 자르지 않고 그대로 본다. 어떤 라벨이 붙어 있는지 알아야 다음부터 무엇으로 거를지 정할 수 있다. 라벨을 보고 나면 읽기 좋게 자른다 — 아래 줄은 가이드가 미검증으로 표시했다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② 라벨을 보고 나서 필요한 줄만 자른다"
|
||||
```bash label="[lab host] ② 라벨을 보고 나서 필요한 줄만 자른다"
|
||||
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]'
|
||||
@@ -284,17 +299,17 @@ Admin API 세션 목록의 id jiv3rVZi1VeaO07oVJkL_MYW
|
||||
|
||||
**목적** — DB 행과 캐시 엔트리를 동시에 0 으로 만들어, 뒤에 세는 숫자가 이 실험이 만든 것만 담게 한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 온라인·오프라인 세션 행을 지운다"
|
||||
```bash label="[lab host] ① 온라인·오프라인 세션 행을 지운다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "delete from offline_user_session"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 재시작 시각을 남기고 롤아웃을 건다"
|
||||
```bash label="[lab host] ② 재시작 시각을 남기고 롤아웃을 건다"
|
||||
date '+%H:%M:%S 재시작'
|
||||
kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 새 파드가 다 설 때까지 블록한다"
|
||||
```bash label="[lab host] ③ 새 파드가 다 설 때까지 블록한다"
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s
|
||||
```
|
||||
|
||||
@@ -316,7 +331,7 @@ kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s
|
||||
|
||||
### 파드가 새로 떴고 IP 가 바뀌었는가
|
||||
|
||||
```bash label="[kc-lab-1] 새 파드와 새 IP 를 다시 잡는다"
|
||||
```bash label="[lab host] 새 파드와 새 IP 를 다시 잡는다"
|
||||
kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
@@ -327,7 +342,7 @@ echo "$K0 $K1"
|
||||
|
||||
### DB 에 세션이 한 행도 없는가
|
||||
|
||||
```bash label="[kc-lab-1] 남은 행을 센다"
|
||||
```bash label="[lab host] 남은 행을 센다"
|
||||
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"
|
||||
```
|
||||
@@ -342,7 +357,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
상주 탐침 파드를 띄운다. Keycloak 이미지에 `curl` 이 없고, 토큰을 단계 사이로 넘겨야 하며, Service 로 보내면 어느 노드가 처리했는지 알 수 없다. 이 실험의 질문 자체가 어느 노드인가이므로 파드 IP 로 직접 친다.
|
||||
|
||||
```bash label="[kc-lab-1] 탐침 파드를 띄우고 그 안의 셸로 들어간다"
|
||||
```bash label="[lab host] 탐침 파드를 띄우고 그 안의 셸로 들어간다"
|
||||
kubectl -n keycloak-lab run kc-probe --rm -it --restart=Never \
|
||||
--image=curlimages/curl:8.11.1 \
|
||||
--env="K0=$K0" --env="K1=$K1" \
|
||||
@@ -353,7 +368,7 @@ kubectl -n keycloak-lab run kc-probe --rm -it --restart=Never \
|
||||
|
||||
셸에서 `exit` 하면 `--rm` 이 파드를 지운다. 비밀번호는 명령 치환으로 넘기므로 값이 터미널에도 셸 히스토리에도 남지 않는다. 존재와 길이만 밖에서 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] 비밀번호의 길이만 센다"
|
||||
```bash label="[lab host] 비밀번호의 길이만 센다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
|
||||
```
|
||||
@@ -400,9 +415,9 @@ AT=$(echo "$R" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
|
||||
echo "refresh=${#RT}자 access=${#AT}자"
|
||||
```
|
||||
|
||||
`refresh=1187자 access=2043자` 같은 모양이 나온다. 길이가 `0자` 면 로그인이 실패한 것이고 `echo "$R"` 로 에러 본문을 본다.
|
||||
`refresh=1187자 access=2043자` 같은 모양이 나온다. **그 두 수를 기준값으로 삼지 않는다** — 2026-09-17 에 다시 세운 실험대에서는 `refresh=612자 access=758자` 였다(observed). 담긴 클레임과 서명 길이에 따라 달라지므로 **보는 것은 `0자` 가 아니라는 것 하나**다. `0자` 면 로그인이 실패한 것이고 `echo "$R"` 로 에러 본문을 본다.
|
||||
|
||||
JWT 의 가운데 토막이 클레임이다. 먼저 통째로 디코드해 눈으로 보고 그다음에 sid 만 잘라낸다. 두 줄 다 미검증이다(unknown).
|
||||
JWT 의 가운데 토막이 클레임이다. 먼저 통째로 디코드해 눈으로 보고 그다음에 sid 만 잘라낸다. **두 줄 다 2026-09-17 에 쳐서 확인했다**(observed).
|
||||
|
||||
```sh label="[탐침 파드] ③ 클레임을 통째로 디코드해 본다"
|
||||
echo "$AT" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
|
||||
@@ -414,9 +429,27 @@ SID=$(echo "$AT" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null \
|
||||
echo "SID=$SID"
|
||||
```
|
||||
|
||||
base64 패딩 때문에 끝이 깨져 보일 수 있고(`2>/dev/null` 이 그 불평을 지운다), `sid` 는 앞쪽에 있어서 대개 보인다.
|
||||
base64 패딩 때문에 끝이 깨져 보일 수 있고(`2>/dev/null` 이 그 불평을 지운다), `sid` 는 앞쪽에 있어서 대개 보인다. 2026-09-17 실측에서는 마지막 `"}` 두 글자가 잘렸다(observed).
|
||||
|
||||
같은 sid 가 두 노드 모두에서 보이는지 물으려면 `admin-cli` 의 내부 id 가 필요하다. 응답을 한 번 그대로 보고 무엇을 자르는지 눈으로 본 다음 잘라낸다. 잘라내는 줄은 미검증이다(unknown).
|
||||
```text
|
||||
0EPyjFf8PwNJ-1q7","scope":"profile email
|
||||
```
|
||||
|
||||
끝까지 보려면 패딩을 채운다. 이 형태로 디코드하면 클레임이 온전히 나온다(observed).
|
||||
|
||||
```sh label="[탐침 파드] ③b 패딩을 채워 끝까지 디코드한다"
|
||||
P=$(echo "$AT" | cut -d. -f2)
|
||||
case $(( ${#P} % 4 )) in 2) P="$P==";; 3) P="$P=";; esac
|
||||
echo "$P" | tr '_-' '/+' | base64 -d; echo
|
||||
```
|
||||
|
||||
```text
|
||||
{"exp":1789620347,"iat":1789620287,"jti":"onltro:ba0c11cf-5e4a-a378-7268-4efd5cef32b8","iss":"https://auth.hyeonworks.com/realms/master","typ":"Bearer","azp":"admin-cli","sid":"jbFsOn6E0EPyjFf8PwNJ-1q7","scope":"profile email"}
|
||||
```
|
||||
|
||||
클레임 이름이 `azp exp iat iss jti scope sid typ` 여덟이고 `sub` 이 없다는 것도 이 형태로 확인했다(observed).
|
||||
|
||||
같은 sid 가 두 노드 모두에서 보이는지 물으려면 `admin-cli` 의 내부 id 가 필요하다. 응답을 한 번 그대로 보고 무엇을 자르는지 눈으로 본 다음 잘라낸다. **잘라내는 줄도 2026-09-17 에 쳐서 확인했다**(observed).
|
||||
|
||||
```sh label="[탐침 파드] ⑤ 클라이언트 목록 응답을 그대로 본다"
|
||||
curl -s -H "Authorization: Bearer $AT" \
|
||||
@@ -441,7 +474,14 @@ for H in "$K0" "$K1"; do
|
||||
done
|
||||
```
|
||||
|
||||
이 `for` 루프도 미검증이다(unknown). 실측은 이렇다(observed, `01-cross-node-session.txt`).
|
||||
**이 `for` 루프도 2026-09-17 에 쳐서 확인했다**(observed). 그때는 양쪽이 `1` 을 냈다.
|
||||
|
||||
```text
|
||||
10.42.1.7 : 1
|
||||
10.42.0.13 : 1
|
||||
```
|
||||
|
||||
원래 실행의 실측은 이렇다(observed, `01-cross-node-session.txt`).
|
||||
|
||||
```text
|
||||
=== [3] 같은 sid 가 두 노드 모두에서 보이는가 ===
|
||||
@@ -539,7 +579,7 @@ exit
|
||||
|
||||
0b 에서 `exit` 했으므로 `--rm` 이 탐침 파드를 이미 지웠다. 0c 와 0d 는 파드 안에서 치므로 같은 명령으로 다시 띄운다.
|
||||
|
||||
```bash label="[kc-lab-1] 탐침 파드를 다시 띄운다"
|
||||
```bash label="[lab host] 탐침 파드를 다시 띄운다"
|
||||
kubectl -n keycloak-lab run kc-probe --rm -it --restart=Never \
|
||||
--image=curlimages/curl:8.11.1 \
|
||||
--env="K0=$K0" --env="K1=$K1" \
|
||||
@@ -587,7 +627,7 @@ keycloak-0 에 로그인 5회 7.0 5.0
|
||||
|
||||
대각선이다. 한 번에 한 쪽만 늘고, `7 + 5 = 12` 로 DB 총계와 맞으므로 어느 엔트리도 두 번 세어지지 않았다.
|
||||
|
||||
```bash label="[kc-lab-1] DB 총계를 센다"
|
||||
```bash label="[lab host] DB 총계를 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select count(*) from offline_user_session where offline_flag='0'"
|
||||
```
|
||||
@@ -598,7 +638,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
0b·0c 까지는 추론이다. 여기서 둘째 주입인 문장 로깅을 켠다. 앞의 세 시험이 끝난 지금 켜는 것이고, 시험 0d 가 끝나면 이 절의 마지막에서 곧바로 끈다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 문장 로깅과 클라이언트 주소 접두사를 켜고 같은 명령에서 reload 한다"
|
||||
```bash label="[lab host] ① 문장 로깅과 클라이언트 주소 접두사를 켜고 같은 명령에서 reload 한다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "alter system set log_statement='all'" \
|
||||
-c "alter system set log_line_prefix='%m [%p] %h '" \
|
||||
@@ -607,7 +647,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
`pg_reload_conf` 가 `t` 를 돌려준다. `%h` 가 클라이언트 주소를 로그 줄 앞에 남기는데, 이것이 없으면 어느 파드가 보낸 질의인지 구별할 수 없어 이 시험의 판정이 성립하지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 두 설정이 실제로 적용됐는지 읽는다"
|
||||
```bash label="[lab host] ② 두 설정이 실제로 적용됐는지 읽는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "show log_statement" -c "show log_line_prefix"
|
||||
```
|
||||
@@ -650,7 +690,7 @@ curl -s -o /dev/null -w '%{http_code}\n' -X POST "http://$K1:8080$TOK" \
|
||||
|
||||
`%h` 가 남긴 IP 로 걸러 `keycloak-1` 이 보낸 것만 본다. 거르는 명령은 관찰용 터미널에서 치는데 거기에는 `K0`·`K1` 이 없다. 두 값은 「주입 검증」에서 잡았는데 그 터미널을 지금 탐침 파드 셸이 붙잡고 있으므로, 관찰용 터미널에서 두 줄을 다시 친다.
|
||||
|
||||
```bash label="[kc-lab-1] 관찰용 터미널에서도 파드 IP 를 잡는다"
|
||||
```bash label="[lab host] 관찰용 터미널에서도 파드 IP 를 잡는다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
echo "$K0 $K1"
|
||||
@@ -658,11 +698,17 @@ echo "$K0 $K1"
|
||||
|
||||
두 값이 「주입 검증」에서 본 것과 같아야 한다. 이 두 줄을 건너뛰고 다음 블록을 치면 `grep "$K1"` 이 `grep ""` 가 되어 모든 줄이 통과하므로, 두 파드가 날린 문장을 `keycloak-1` 만 걸러 낸 것으로 읽게 되고 뒤에서 세는 건수도 양쪽이 같은 값으로 나온다. 화면에는 아무 경고도 안 뜬다.
|
||||
|
||||
```bash label="[kc-lab-1] 반대편 노드가 날린 문장만 추린다"
|
||||
```bash label="[lab host] 반대편 노드가 날린 문장만 추린다"
|
||||
kubectl -n keycloak-lab logs deploy/postgres --since=60s \
|
||||
| grep "$K1" | grep 'LOG: execute'
|
||||
```
|
||||
|
||||
:::warning
|
||||
|
||||
**요청을 보내자마자 이 명령을 치면 한 줄도 안 나온다.** 이 트랜잭션은 마지막에서 두 번째 문장이 `SET LOCAL synchronous_commit TO OFF` 라 커밋이 미뤄지고, 로그 줄도 그만큼 늦게 나온다. 2026-09-17 에 요청 직후에 쳤더니 `keycloak-1` 쪽에서는 디스커버리 질의 한 줄만 나왔고, 같은 필터를 몇 초 뒤에 다시 치니 아래 여덟 줄이 다 나왔다(observed). **그 첫 화면을 그대로 읽으면 「반대편은 DB 를 안 읽었다 = 복제였다」는 정반대 결론이 나온다.** 몇 초 기다렸다 친다.
|
||||
|
||||
:::
|
||||
|
||||
실측은 이렇다(observed, `04-read-path-sql.txt`).
|
||||
|
||||
```text
|
||||
@@ -676,9 +722,9 @@ kubectl -n keycloak-lab logs deploy/postgres --since=60s \
|
||||
COMMIT
|
||||
```
|
||||
|
||||
`keycloak-1` 은 세션을 DB 에서 읽고 DB 에 쓴다. 파라미터는 `DETAIL` 줄에 있다. sid 는 ③ 이 `SID=` 로 화면에 찍은 값을 옮겨 넣는다 — 그 변수는 탐침 파드 안에만 있어서 `[kc-lab-1]` 셸에서는 `$SID` 가 빈 문자열이다. 로그인할 때마다 새로 생기는 값이기도 하다.
|
||||
`keycloak-1` 은 세션을 DB 에서 읽고 DB 에 쓴다. 파라미터는 `DETAIL` 줄에 있다. sid 는 ③ 이 `SID=` 로 화면에 찍은 값을 옮겨 넣는다 — 그 변수는 탐침 파드 안에만 있어서 `[lab host]` 셸에서는 `$SID` 가 빈 문자열이다. 로그인할 때마다 새로 생기는 값이기도 하다.
|
||||
|
||||
```bash label="[kc-lab-1] 그 sid 가 들어간 줄만 앞에서 120자씩 본다"
|
||||
```bash label="[lab host] 그 sid 가 들어간 줄만 앞에서 120자씩 본다"
|
||||
kubectl -n keycloak-lab logs deploy/postgres --since=60s \
|
||||
| grep '{{SID}}' | cut -c1-120
|
||||
```
|
||||
@@ -698,11 +744,18 @@ kubectl -n keycloak-lab logs deploy/postgres --since=60s \
|
||||
|
||||
증거 파일에는 IP 대신 `[keycloak-0]` `[keycloak-1]` 이 적혀 있다. 원래 실행 스크립트가 `sed` 로 IP 를 파드 이름으로 바꿔 놓은 것이고, 따라 하는 화면에는 `10.42.0.35` 같은 IP 가 그대로 나온다. pid 도 본다 — `81407` 은 `keycloak-0` 의 연결, `81376` 은 `keycloak-1` 의 연결이며 pid 가 트랜잭션의 경계를 가른다.
|
||||
|
||||
이 갱신 트랜잭션은 `01:12:34.934` 의 `BEGIN` 에서 `01:12:34.947` 의 `COMMIT` 까지 13밀리초다. `BEGIN` 과 `COMMIT` 은 sid 를 파라미터로 달지 않아서 위 `grep` 에 안 걸린다. 그래서 화면에 남는 마지막 줄이 `.946` 이고, 거기까지만 세면 12 가 나온다. 경계 두 줄은 같은 pid `81376` 연결에서 나왔고, 실험 기록에 `pid=… |` 꼴로 옮겨 적힌 것으로만 남아 있다(observed). 자기 화면에서 그 둘을 보려면 sid 필터를 빼고 로그에 찍힌 그 pid 로 다시 걸러야 하는데, 그 명령은 원 가이드에 없다(unknown). 그 두 줄이 하는 일은 트랜잭션의 경계를 긋는 것이다. 뒤에 나오는 `SET LOCAL synchronous_commit TO OFF` 가 같은 트랜잭션 안에서 `COMMIT` 직전에 나왔다는 판정이 거기서 나오고, 원 가이드는 그 확인을 「pid 로 경계를 확인했다」로 적는다.
|
||||
이 갱신 트랜잭션은 `01:12:34.934` 의 `BEGIN` 에서 `01:12:34.947` 의 `COMMIT` 까지 13밀리초다. `BEGIN` 과 `COMMIT` 은 sid 를 파라미터로 달지 않아서 위 `grep` 에 안 걸린다. 그래서 화면에 남는 마지막 줄이 `.946` 이고, 거기까지만 세면 12 가 나온다. 경계 두 줄은 같은 pid `81376` 연결에서 나왔고, 실험 기록에 `pid=… |` 꼴로 옮겨 적힌 것으로만 남아 있다(observed). 자기 화면에서 그 둘을 보려면 sid 필터를 빼고 로그에 찍힌 그 pid 로 다시 거른다. 원 가이드에는 그 명령이 없어서 2026-09-17 에 만들어 쳐 봤다(observed).
|
||||
|
||||
```bash label="[lab host] 그 pid 의 트랜잭션 경계만 본다"
|
||||
kubectl -n keycloak-lab logs deploy/postgres --since=5m \
|
||||
| grep '{{PID}}' | grep -E 'BEGIN|COMMIT|synchronous_commit'
|
||||
```
|
||||
|
||||
`{{PID}}` 는 위 `DETAIL` 줄의 대괄호 안 숫자다 — 그 실험대에서는 `[915]` 였다. 같은 pid 의 `BEGIN` 과 `COMMIT` 사이에 여덟 문장이 들어 있는 것이 보인다. 그 두 줄이 하는 일은 트랜잭션의 경계를 긋는 것이다. 뒤에 나오는 `SET LOCAL synchronous_commit TO OFF` 가 같은 트랜잭션 안에서 `COMMIT` 직전에 나왔다는 판정이 거기서 나오고, 원 가이드는 그 확인을 「pid 로 경계를 확인했다」로 적는다.
|
||||
|
||||
파드별 질의 건수는 미검증 형태로 센다(unknown). 여기서도 sid 는 ③ 이 찍은 자기 값이다.
|
||||
|
||||
```bash label="[kc-lab-1] 두 파드가 각각 몇 줄을 날렸는지 센다"
|
||||
```bash label="[lab host] 두 파드가 각각 몇 줄을 날렸는지 센다"
|
||||
kubectl -n keycloak-lab logs deploy/postgres --since=60s \
|
||||
| grep '{{SID}}' | grep -c "$K0"
|
||||
kubectl -n keycloak-lab logs deploy/postgres --since=60s \
|
||||
@@ -726,7 +779,7 @@ kubectl -n keycloak-lab logs deploy/postgres --since=60s \
|
||||
|
||||
13밀리초짜리 그 트랜잭션에는 셋이 들어 있었다. 낙관적 락(`VERSION` 컬럼), `FOR NO KEY UPDATE ... SKIP LOCKED`, 그리고 `SET LOCAL synchronous_commit TO OFF` 다. 전역 설정은 다르다.
|
||||
|
||||
```bash label="[kc-lab-1] 전역 설정을 읽는다"
|
||||
```bash label="[lab host] 전역 설정을 읽는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "show synchronous_commit"
|
||||
```
|
||||
@@ -741,7 +794,7 @@ exit
|
||||
|
||||
문장 로깅은 곧바로 끈다.
|
||||
|
||||
```bash label="[kc-lab-1] 문장 로깅을 끄고 꺼졌는지 읽는다"
|
||||
```bash label="[lab host] 문장 로깅을 끄고 꺼졌는지 읽는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "alter system reset log_statement" -c "alter system reset log_line_prefix" \
|
||||
-c "select pg_reload_conf()"
|
||||
@@ -759,12 +812,12 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**목적** — 다음 실험이 옛 설정 위에서 돌지 않게 한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 두 설정을 읽는다"
|
||||
```bash label="[lab host] ① 두 설정을 읽는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "show log_statement" -c "show log_line_prefix"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② none 이 아니면 두 설정을 되돌리고 reload 한다"
|
||||
```bash label="[lab host] ② none 이 아니면 두 설정을 되돌리고 reload 한다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "alter system reset log_statement" -c "alter system reset log_line_prefix" \
|
||||
-c "select pg_reload_conf()"
|
||||
@@ -780,12 +833,12 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**목적** — DB 행과 캐시 엔트리를 함께 비워 다음 실험의 출발값을 0 으로 만든다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 세션 행을 지운다"
|
||||
```bash label="[lab host] ① 세션 행을 지운다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "delete from offline_user_session"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드를 갈아 끼워 캐시를 비운다"
|
||||
```bash label="[lab host] ② 파드를 갈아 끼워 캐시를 비운다"
|
||||
kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s
|
||||
```
|
||||
@@ -809,7 +862,7 @@ kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s
|
||||
|
||||
탐침 파드가 지워지지 않았으면 직접 지운다.
|
||||
|
||||
```bash label="[kc-lab-1] --rm 이 안 먹었을 때"
|
||||
```bash label="[lab host] --rm 이 안 먹었을 때"
|
||||
kubectl -n keycloak-lab delete pod kc-probe --ignore-not-found
|
||||
```
|
||||
|
||||
@@ -838,7 +891,10 @@ kubectl -n keycloak-lab delete pod kc-probe --ignore-not-found
|
||||
이 절차의 숫자는 `2026-09-04 09:52–10:14 KST` 에 돈 한 번의 실행에서 나왔다(observed).
|
||||
|
||||
- (observed) 파드 IP `10.42.1.43`·`10.42.0.35`, 뷰 ID `5` 와 멤버 `(2)`, `jgroups_ping` 의 `coord = t` 하나, 캐시 델타 전량, `7 + 5 = 12`, `keycloak-1` 이 날린 SQL 여덟 줄, pid `81407`/`81376`, 비밀번호 길이 `19`.
|
||||
- (unknown) `tr ',' '\n' | grep -E` 로 자른 Prometheus 출력, JWT 를 디코드해 sid 를 뽑는 `sed` 줄, `CID` 를 뽑는 줄, 두 노드에 `grep -c` 를 도는 `for` 루프, 파드별 질의 건수를 세는 두 줄. 가이드가 미검증으로 표시했고 원래 실행은 스크립트로 했다.
|
||||
- (observed) 2026-09-17 에 기반 가이드 7단계로 실험대를 새로 세우고 이 절차를 처음부터 다시 밟았다. 가이드가 미검증으로 표시했던 줄이 전부 돌았다 — `tr ',' '\n' | grep -E` 로 자른 Prometheus 출력, JWT 를 디코드해 sid 를 뽑는 `sed` 줄, `CID` 를 뽑는 줄, 두 노드에 `grep -c` 를 도는 `for` 루프.
|
||||
- (observed) 같은 날 다시 나온 판정 — 반대편 노드의 refresh 가 `200` 이고 sid 가 같은 것, 반대편 로그아웃 뒤 발급 노드가 `400 invalid_grant / Session not active` 를 내는 것, 로그인 한 번에 발급 노드만 `+1` 이고 반대편은 `+0` 인 것, 로그인을 몰아준 쪽만 늘어 `9 + 5 = 14` 가 DB 총계 `14` 와 맞는 것, 반대편이 날린 SQL 여덟 줄이 같은 순서로 나온 것.
|
||||
- (observed) 같은 날 새로 드러난 것 — 요청 직후에 로그를 거르면 반대편 문장이 한 줄도 안 나온다는 것, pid 로 거르면 `BEGIN`~`COMMIT` 경계가 보인다는 것, 토큰 길이가 `612자`·`758자` 로 원래 실행과 다르다는 것.
|
||||
- (unknown) 파드별 질의 건수를 세는 두 줄은 이번에도 그대로 치지 않았다. 원래 실행은 스크립트로 했다.
|
||||
- 트랜잭션 경계 시각 `01:12:34.934` 와 `01:12:34.947` 은 실험 기록 한 벌에만 있다. `04-read-path-sql.txt` 에 보존된 것은 sid 가 걸린 `DETAIL` 줄과 시각 없는 문장 목록이라 타임스탬프가 붙은 `BEGIN`/`COMMIT` 이 없다. 그래서 13밀리초를 증거 원문으로 다시 확인할 수는 없다.
|
||||
- 캐시 설정은 파일에서 읽을 수 없다. `cache-ispn.xml` 에는 `<transport/>` 뿐이고, 「세션은 DB 로 공유된다」는 판정은 설정을 읽어서가 아니라 동작을 측정해서 얻었다.
|
||||
- 스크립트를 안 쓰는 까닭도 측정 실패에서 나왔다(observed). `kubectl run --rm -i ... | grep` 로 받았더니 중간 조각이 통째로 사라져 `keycloak-1` 의 스냅샷과 다음 마커가 함께 없어졌고, 전값이 0 으로 잡히면서 가짜 델타가 만들어졌다. 그때 리포트는 `keycloak-1` 이 `+9`, `+7` 증가한 것처럼 보였다 — 없는 복제가 있는 것처럼 보이는 오류다. 다른 하나는 중첩 인용이다. `ssh host '... $VAR ...'` 안에 다시 `sh -c "..."` 를 넣으면 인용이 세 겹이 되어 치환이 조용히 깨졌고, 첫 시도에서 파드 IP 가 빈 문자열이 되어 아무 출력도 나오지 않았다.
|
||||
|
||||
+136
-45
@@ -43,11 +43,13 @@ NetworkPolicy 로 8080 과 9000 만 열어 JGroups 트랜스포트인 TCP 7800
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
`kubectl` 은 `[kc-lab-1]` 에서 친다. `conntrack` 은 노드 자체를 건드리는 명령이라 게스트 셸이 필요하고 두 노드 모두에서 봐야 한다 — 이쪽 노드는 그대로 치고 반대 노드는 `ssh kc-lab-2` 로 붙어서 친다. `kubectl` 에는 `sudo` 를 붙이지 않는다. root 홈에 `~/.kube/config` 가 없어서 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝난다. 코드블록마다 어느 셸인지 붙여 두었다.
|
||||
`kubectl` 은 `[lab host]` 에서 친다. `conntrack` 은 노드 자체를 건드리는 명령이라 게스트 셸이 필요하고 두 노드 모두에서 봐야 한다 — 이쪽 노드는 그대로 치고 반대 노드는 `ssh kc-lab-2` 로 붙어서 친다. `kubectl` 에는 `sudo` 를 붙이지 않는다. root 홈에 `~/.kube/config` 가 없어서 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝난다. 코드블록마다 어느 셸인지 붙여 두었다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
주입에 쓰는 매니페스트 경로 `deploy/lab/k8s/a1-block-jgroups-transport.yaml` 는 저장소 상대경로다. 체크아웃을 `kc-lab-1` 의 어디에 뒀는지는 원 가이드에 없고 거기로 옮기는 명령도 없으므로(unknown), 이 상대경로가 그대로 통하는 디렉터리에서 시작한다. 다른 디렉터리에서 치면 `cat` 도 `kubectl apply` 도 파일을 못 찾고 끝난다.
|
||||
|
||||
터미널은 둘을 연다. 하나는 임시 curl 파드용, 하나는 관찰용이다. 그래서 `[kc-lab-1]` 라벨이 붙은 블록이 `[탐침 파드]` 블록 사이에 끼어 있으면 **관찰용 터미널에서 친다** — 파드 셸을 나가라는 뜻이 아니다. 나가라고 할 때는 `exit` 를 블록으로 따로 적는다.
|
||||
터미널은 둘을 연다. 하나는 임시 curl 파드용, 하나는 관찰용이다. 그래서 `[lab host]` 라벨이 붙은 블록이 `[탐침 파드]` 블록 사이에 끼어 있으면 **관찰용 터미널에서 친다** — 파드 셸을 나가라는 뜻이 아니다. 나가라고 할 때는 `exit` 를 블록으로 따로 적는다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -82,7 +84,7 @@ A-0 은 세션이 Infinispan 복제가 아니라 PostgreSQL 로 공유된다고
|
||||
|
||||
**이건 상태를 부수는 실험이다.** Keycloak 클러스터를 실제로 분단시키고 파드를 재시작한다. **실험대에서만 한다.** 중간에 그만두려면 아래 한 줄이면 된다.
|
||||
|
||||
```bash label="[kc-lab-1] 중간에 그만둘 때 치는 한 줄"
|
||||
```bash label="[lab host] 중간에 그만둘 때 치는 한 줄"
|
||||
kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport
|
||||
```
|
||||
|
||||
@@ -98,15 +100,15 @@ kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport
|
||||
|
||||
**무엇을 보는가** — 파드 둘의 상태와 배치, 그리고 뒤에서 쓸 파드 주소.
|
||||
|
||||
```bash label="[kc-lab-1] ① 노드를 본다"
|
||||
```bash label="[lab host] ① 노드를 본다"
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드가 어느 노드에 있는지 본다"
|
||||
```bash label="[lab host] ② 파드가 어느 노드에 있는지 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 파드 주소 두 개를 변수에 담는다"
|
||||
```bash label="[lab host] ③ 파드 주소 두 개를 변수에 담는다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
echo "$K0 $K1"
|
||||
@@ -120,7 +122,7 @@ echo "$K0 $K1"
|
||||
|
||||
**무엇을 보는가** — 네임스페이스에 이미 걸린 NetworkPolicy.
|
||||
|
||||
```bash label="[kc-lab-1] 네임스페이스의 정책 목록"
|
||||
```bash label="[lab host] 네임스페이스의 정책 목록"
|
||||
kubectl -n keycloak-lab get networkpolicy
|
||||
```
|
||||
|
||||
@@ -136,7 +138,7 @@ No resources found in keycloak-lab namespace.
|
||||
|
||||
**무엇을 보는가** — 두 노드가 같은 멤버 목록을 찍고 있는지.
|
||||
|
||||
```bash label="[kc-lab-1] 양쪽 로그에서 마지막 클러스터 뷰 한 줄씩"
|
||||
```bash label="[lab host] 양쪽 로그에서 마지막 클러스터 뷰 한 줄씩"
|
||||
kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
|
||||
kubectl -n keycloak-lab logs keycloak-1 | grep ISPN000094 | tail -1
|
||||
```
|
||||
@@ -154,7 +156,7 @@ kubectl -n keycloak-lab logs keycloak-1 | grep ISPN000094 | tail -1
|
||||
|
||||
**무엇을 보는가** — `JGROUPS_PING` 의 세 열.
|
||||
|
||||
```bash label="[kc-lab-1] 디스커버리 테이블 세 열"
|
||||
```bash label="[lab host] 디스커버리 테이블 세 열"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select name, ip, coord from jgroups_ping order by name"
|
||||
```
|
||||
@@ -175,20 +177,20 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**무엇을 보는가** — 각 노드가 스스로 세는 클러스터 크기.
|
||||
|
||||
```bash label="[kc-lab-1] ① 한 줄짜리 JSON 을 통째로 본다"
|
||||
```bash label="[lab host] ① 한 줄짜리 JSON 을 통째로 본다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
|
||||
```
|
||||
|
||||
라벨을 보고 나면 읽기 좋게 자른다. 아래 두 줄은 가이드가 미검증으로 표시했고(unknown), 둘째 줄은 `jq` 가 깔려 있는 환경용이라 이 실험대에서는 쓸 수 없다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 라벨을 보고 나서 필요한 줄만 자른다"
|
||||
```bash label="[lab host] ② 라벨을 보고 나서 필요한 줄만 자른다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
|
||||
| tr ',' '\n' | grep -E '"pod":|^"[0-9]'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ jq 가 있는 환경이라면 이 형태 — 이 실험대에는 jq 가 없다"
|
||||
```bash label="[lab host] ③ jq 가 있는 환경이라면 이 형태 — 이 실험대에는 jq 가 없다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
|
||||
| jq -r '.data.result[] | "\(.metric.pod) \(.metric.node) \(.value[1])"'
|
||||
@@ -198,7 +200,7 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
|
||||
JGroups 카운터도 지금 0 인 것을 봐 둔다.
|
||||
|
||||
```bash label="[kc-lab-1] 병합 이벤트 계수기"
|
||||
```bash label="[lab host] 병합 이벤트 계수기"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_jgroups_merge3_get_num_merge_events'
|
||||
```
|
||||
@@ -216,7 +218,7 @@ vendor_jgroups_fd_sock2_get_num_suspected_members 0.0 (양쪽 노드)
|
||||
|
||||
임시 파드를 띄우고 그 안에서 A-0 과 같은 순서로 로그인·refresh·로그아웃을 친다. 임시 파드인 까닭은 셋이다. Keycloak 이미지에 `curl` 이 없어 Keycloak 파드 안에서는 못 치고, 토큰을 단계 사이로 넘겨야 하니 한 셸 안에서 다 끝내야 하며, Service 로 보내면 어느 노드가 처리했는지 알 수 없다. 이 실험의 질문 자체가 「어느 노드인가」라서 파드 주소로 직접 친다.
|
||||
|
||||
```bash label="[kc-lab-1] 임시 curl 파드를 띄우고 그 안의 셸로 들어간다"
|
||||
```bash label="[lab host] 임시 curl 파드를 띄우고 그 안의 셸로 들어간다"
|
||||
kubectl -n keycloak-lab run kc-probe --rm -it --restart=Never \
|
||||
--image=curlimages/curl:8.11.1 \
|
||||
--env="K0=$K0" --env="K1=$K1" \
|
||||
@@ -293,7 +295,7 @@ exit
|
||||
|
||||
**목적** — 8080 과 9000 은 열어 둔 채 7800 으로 오는 새 연결만 막는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 매니페스트를 먼저 읽는다"
|
||||
```bash label="[lab host] ① 매니페스트를 먼저 읽는다"
|
||||
cat deploy/lab/k8s/a1-block-jgroups-transport.yaml
|
||||
```
|
||||
|
||||
@@ -321,7 +323,7 @@ spec:
|
||||
|
||||
매니페스트 안에 `metadata.namespace` 가 `keycloak-lab` 으로 적혀 있다. ② 의 `apply` 에는 `-n` 이 없으니 네임스페이스는 이 줄이 정한다. 손으로 옮겨 적다 이 줄을 빠뜨리면 정책이 `default` 네임스페이스에 걸린다. 거기에는 `app=keycloak` 인 파드가 없으니 정책은 만들어지는데 아무 파드도 안 잡힌다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 적용하고 시각을 남긴다"
|
||||
```bash label="[lab host] ② 적용하고 시각을 남긴다"
|
||||
kubectl apply -f deploy/lab/k8s/a1-block-jgroups-transport.yaml
|
||||
date '+%H:%M:%S 적용'
|
||||
```
|
||||
@@ -343,14 +345,14 @@ networkpolicy.networking.k8s.io/a1-block-jgroups-transport created
|
||||
|
||||
### 1. 정책이 어떤 파드를 잡았는가
|
||||
|
||||
```bash label="[kc-lab-1] 정책 목록과 상세"
|
||||
```bash label="[lab host] 정책 목록과 상세"
|
||||
kubectl -n keycloak-lab get networkpolicy
|
||||
kubectl -n keycloak-lab describe networkpolicy a1-block-jgroups-transport
|
||||
```
|
||||
|
||||
`To Port` 목록에 7800 이 없는 것, 그리고 `PodSelector` 가 `app=keycloak` 인 것을 본다. 오타로 아무 파드도 안 잡히면 정책은 걸렸는데 아무 일도 일어나지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] 파드가 죽지 않았는지 본다"
|
||||
```bash label="[lab host] 파드가 죽지 않았는지 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||||
```
|
||||
|
||||
@@ -358,7 +360,7 @@ kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||||
|
||||
### 2. 열어 둔 포트는 살아 있고 막은 포트는 죽었는가
|
||||
|
||||
```bash label="[kc-lab-1] 임시 파드를 띄운다"
|
||||
```bash label="[lab host] 임시 파드를 띄운다"
|
||||
kubectl -n keycloak-lab run kc-probe --rm -it --restart=Never \
|
||||
--image=curlimages/curl:8.11.1 --env="K0=$K0" --env="K1=$K1" --command -- sh
|
||||
```
|
||||
@@ -387,7 +389,7 @@ exit
|
||||
|
||||
### 3. 그런데 클러스터가 안 깨졌다
|
||||
|
||||
```bash label="[kc-lab-1] 두 노드가 아는 멤버 수를 다시 본다"
|
||||
```bash label="[lab host] 두 노드가 아는 멤버 수를 다시 본다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
|
||||
```
|
||||
@@ -402,7 +404,7 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
|
||||
여기서 실험 실패라고 결론 내리면 틀린다. 파드 안 소켓을 본다. Keycloak 이미지에는 `ss` 도 없으므로 `/proc` 을 직접 읽는다.
|
||||
|
||||
```bash label="[kc-lab-1] 7800 은 16진수로 1E78 이다"
|
||||
```bash label="[lab host] 7800 은 16진수로 1E78 이다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- cat /proc/net/tcp6 | grep 1E78
|
||||
```
|
||||
|
||||
@@ -429,13 +431,42 @@ keycloak-1: ...23002A0A:9C57 ...2B012A0A:1E78 01
|
||||
|
||||
### 4. conntrack 을 두 노드 모두에서 본다
|
||||
|
||||
여기부터는 게스트 셸이다. lab host 에서 먼저 들어간다.
|
||||
|
||||
```bash label="[lab host] 이쪽 노드에 붙는다"
|
||||
ssh kc-lab-1
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] 이 노드의 conntrack 표를 본다"
|
||||
sudo conntrack -L 2>/dev/null | grep 7800
|
||||
```
|
||||
|
||||
:::warning
|
||||
|
||||
**게스트에 `conntrack` 이 깔려 있지 않다.** cloud-init 이 까는 것은 `curl` 과 `nftables` 뿐이라 이 명령은 `sudo: conntrack: command not found` 로 끝나는데, **`2>/dev/null` 이 그 한 줄을 지우고 종료 코드도 `0` 이다.** 화면에는 아무것도 안 나오고, 그 빈 화면은 「7800 연결이 없다 = 정책이 먹었다」로 읽힌다 — 이 절이 보여 주려는 것과 정반대다. 2026-09-17 에 갓 만든 게스트에서 그대로 겪었다(observed). 두 노드 모두에 먼저 깐다.
|
||||
|
||||
:::
|
||||
|
||||
```bash label="[lab host] 없으면 먼저 깐다"
|
||||
sudo apt install -y conntrack
|
||||
```
|
||||
|
||||
깔고 나면 문서가 적은 두 줄이 그대로 나온다(2026-09-17, observed).
|
||||
|
||||
```text
|
||||
tcp 6 86398 ESTABLISHED src=10.42.0.13 dst=10.42.1.7 sport=59725 dport=7800 src=10.42.1.7 dst=10.42.0.13 sport=7800 dport=59725 [ASSURED] mark=0 use=1
|
||||
tcp 6 85885 ESTABLISHED src=10.42.0.13 dst=10.42.1.7 sport=38439 dport=57800 src=10.42.1.7 dst=10.42.0.13 sport=57800 dport=38439 [ASSURED] mark=0 use=1
|
||||
```
|
||||
|
||||
**깔지 않고 보는 방법도 있다.** 커널 표를 직접 읽으면 같은 내용이 나온다 — 앞에 `ipv4 2` 두 칸이 더 붙고 `zone=0` 이 더 붙는 것만 다르다(observed).
|
||||
|
||||
```bash label="[kc-lab-1] 패키지 없이 커널 표를 직접 읽는다"
|
||||
sudo grep 7800 /proc/net/nf_conntrack
|
||||
```
|
||||
|
||||
반대편 노드는 붙어서 친다. 아래 세 줄 형태는 이 실험대에서 치지 않았다(unknown) — 원래 실행은 `ssh kc-lab-2 'sudo conntrack -L 2>/dev/null | grep 7800'` 한 줄로 쳤고, 한 줄에 원격 접속과 원격 셸의 인용을 겹쳐 놓는 대신 행동 하나를 명령 하나로 나눴다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 반대편 노드에 붙는다"
|
||||
```bash label="[lab host] ① 반대편 노드에 붙는다"
|
||||
ssh kc-lab-2
|
||||
```
|
||||
|
||||
@@ -443,6 +474,8 @@ ssh kc-lab-2
|
||||
sudo conntrack -L 2>/dev/null | grep 7800
|
||||
```
|
||||
|
||||
**이 노드에도 따로 깔아야 한다.** 패키지는 노드마다이고, 여기서도 안 깔린 채로 치면 위와 같은 빈 화면이 나온다.
|
||||
|
||||
```bash label="[kc-lab-2] ③ 나온다"
|
||||
exit
|
||||
```
|
||||
@@ -460,7 +493,7 @@ exit
|
||||
tcp 6 33 SYN_SENT src=10.42.1.58 dst=10.42.0.35 sport=34824 dport=7800 [UNREPLIED] ...
|
||||
```
|
||||
|
||||
`2>/dev/null` 은 `conntrack` 이 stderr 로 찍는 「N flow entries have been shown」 요약을 지우려는 것이고, 처음에는 빼고 쳐서 그 줄도 한번 본다. 상태 열을 읽는다 — `ESTABLISHED` 는 양방향 통신이 성립해 규칙 평가를 건너뛰고, `[ASSURED]` 는 표가 꽉 차도 안 지워지는 오래된 연결이며, `SYN_SENT [UNREPLIED]` 가 정책이 동작하고 있다는 것을 보여 준다. `dport=57800` 도 ESTABLISHED 로 살아 있다. NetworkPolicy 는 이미 붙어 있는 연결을 떼어내지 못하므로, 보안 사고 대응으로 「지금 당장 이 통신을 끊어라」에 NetworkPolicy 를 걸면 새 연결만 막히고 진행 중인 연결은 계속된다. 같은 7800 을 한 방향만 끊는 A-5 가 NetworkPolicy 대신 `iptables` 의 raw PREROUTING 으로 간 까닭도 여기서 나왔다 — 그 체인은 conntrack 조회보다 먼저 평가된다.
|
||||
`2>/dev/null` 은 `conntrack` 이 stderr 로 찍는 「N flow entries have been shown」 요약을 지우려는 것이고, **처음에는 반드시 빼고 친다** — 위에서 본 `command not found` 가 거기로 나가기 때문이다. 상태 열을 읽는다 — `ESTABLISHED` 는 양방향 통신이 성립해 규칙 평가를 건너뛰고, `[ASSURED]` 는 표가 꽉 차도 안 지워지는 오래된 연결이며, `SYN_SENT [UNREPLIED]` 가 정책이 동작하고 있다는 것을 보여 준다. `dport=57800` 도 ESTABLISHED 로 살아 있다. NetworkPolicy 는 이미 붙어 있는 연결을 떼어내지 못하므로, 보안 사고 대응으로 「지금 당장 이 통신을 끊어라」에 NetworkPolicy 를 걸면 새 연결만 막히고 진행 중인 연결은 계속된다. 같은 7800 을 한 방향만 끊는 A-5 가 NetworkPolicy 대신 `iptables` 의 raw PREROUTING 으로 간 까닭도 여기서 나왔다 — 그 체인은 conntrack 조회보다 먼저 평가된다.
|
||||
|
||||
### 5. conntrack 항목을 튜플 그대로 지운다
|
||||
|
||||
@@ -482,7 +515,7 @@ sudo conntrack -D -p tcp -s 10.42.0.35 -d 10.42.1.43 --sport 50477 --dport 57800
|
||||
|
||||
`kc-lab-2` 에서도 같은 일을 한다. 서버 쪽 노드에는 튜플이 뒤집혀 기록되어 있으므로, 그쪽에 붙어 그쪽 `-L` 출력을 보고 옮긴다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 반대편 노드에 붙는다"
|
||||
```bash label="[lab host] ① 반대편 노드에 붙는다"
|
||||
ssh kc-lab-2
|
||||
```
|
||||
|
||||
@@ -510,12 +543,12 @@ exit
|
||||
|
||||
**목적** — 새로 뜨는 노드가 7800 으로 JOIN 을 보내다 실패하게 만들어 분단을 확실히 만든다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 시각을 남기고 파드를 지운다"
|
||||
```bash label="[lab host] ① 시각을 남기고 파드를 지운다"
|
||||
date '+%H:%M:%S 재시작'
|
||||
kubectl -n keycloak-lab delete pod keycloak-0
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 새 파드와 새 주소를 다시 잡는다"
|
||||
```bash label="[lab host] ② 새 파드와 새 주소를 다시 잡는다"
|
||||
kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
echo "$K0"
|
||||
@@ -537,7 +570,7 @@ keycloak-0 false 10.42.1.67 2026-09-04T02:44:23Z
|
||||
|
||||
## 관찰
|
||||
|
||||
```bash label="[kc-lab-1] 두 노드가 아는 멤버 수"
|
||||
```bash label="[lab host] 두 노드가 아는 멤버 수"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
|
||||
```
|
||||
@@ -551,7 +584,7 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
|
||||
양쪽 다 1 이다. 서로를 멤버로 안 세고 있고, 로그가 까닭을 말한다.
|
||||
|
||||
```bash label="[kc-lab-1] 합류 시도와 클러스터 뷰를 한 번에 본다"
|
||||
```bash label="[lab host] 합류 시도와 클러스터 뷰를 한 번에 본다"
|
||||
kubectl -n keycloak-lab logs keycloak-0 | grep -E "GMS|ISPN000094" | tail -20
|
||||
```
|
||||
|
||||
@@ -567,7 +600,7 @@ ISPN000094: new cluster view [keycloak-0-26403|0] (1) [keycloak-0-26403]
|
||||
|
||||
split brain 은 DB 한 줄로 확인된다.
|
||||
|
||||
```bash label="[kc-lab-1] 코디네이터가 몇인지 센다"
|
||||
```bash label="[lab host] 코디네이터가 몇인지 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select name, ip, coord from jgroups_ping order by name"
|
||||
```
|
||||
@@ -585,18 +618,48 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
분단된 노드는 스스로 트래픽에서 빠진다.
|
||||
|
||||
```bash label="[kc-lab-1] Ready 와 재시작 횟수만 뽑아 본다"
|
||||
kubectl -n keycloak-lab get pods -o custom-columns=\
|
||||
NAME:.metadata.name,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount \
|
||||
```bash label="[lab host] Ready 와 재시작 횟수만 뽑아 본다"
|
||||
kubectl -n keycloak-lab get pods \
|
||||
-o "custom-columns=NAME:.metadata.name,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount" \
|
||||
| grep keycloak
|
||||
```
|
||||
|
||||
실측은 `keycloak-0 false 0`, `keycloak-1 true 0` 이다(observed, `11-service-impact.txt`). 까닭은 헬스 본문에 있다.
|
||||
**`-o` 값을 따옴표로 묶는다.** 묶지 않으면 zsh 가 `[0]` 을 글로브로 읽어 명령이 아예 안 돈다 — 이 실험대의 lab host 가 zsh 다(2026-09-17, observed).
|
||||
|
||||
```bash label="[kc-lab-1] 파드 조건을 본다"
|
||||
```text
|
||||
zsh: no matches found: custom-columns=NAME:.metadata.name,READY:.status.containerStatuses[0].ready,...
|
||||
```
|
||||
|
||||
실측은 `keycloak-0 false 0`, `keycloak-1 true 0` 이다(observed, `11-service-impact.txt`). 2026-09-17 에도 같은 두 줄이 나왔다(observed).
|
||||
|
||||
**그 `false` 는 파드를 지우자마자 나오지 않는다.** readiness 프로브가 `periodSeconds: 10` · `failureThreshold: 3` 이라 헬스가 먼저 `503` 이 되고 그로부터 최대 30초 뒤에 조건이 뒤집힌다. 2026-09-17 에 지운 지 60초 시점에는 헬스가 이미 `503` 인데 파드는 아직 `1/1 Ready=True` 였다(observed). **그 시점 화면만 보고 「분단이 서비스에 안 잡혔다」로 읽지 않는다** — 1~2분 두고 다시 본다.
|
||||
|
||||
까닭은 헬스 본문에 있다.
|
||||
|
||||
```bash label="[lab host] 파드 조건을 본다"
|
||||
kubectl -n keycloak-lab describe pod keycloak-0 | grep -A5 Conditions
|
||||
```
|
||||
|
||||
이 명령이 내는 것은 조건표다(2026-09-17, observed).
|
||||
|
||||
```text
|
||||
Conditions:
|
||||
Type Status
|
||||
PodReadyToStartContainers True
|
||||
Initialized True
|
||||
Ready False
|
||||
ContainersReady False
|
||||
```
|
||||
|
||||
**아래 JSON 은 이 명령의 출력이 아니다.** 헬스 엔드포인트를 직접 쳐야 나온다 — 그 명령이 원 가이드에 없어서 2026-09-17 에 만들어 쳤다(observed).
|
||||
|
||||
```bash label="[lab host] 분단된 노드의 헬스 본문을 직접 받는다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
kubectl -n keycloak-lab run hp --rm -i --restart=Never \
|
||||
--image=curlimages/curl:8.11.1 --quiet --env="K0=$K0" \
|
||||
--command -- sh -c 'curl -s "http://$K0:9000/health/ready"'
|
||||
```
|
||||
|
||||
실측은 이렇다(observed).
|
||||
|
||||
```json
|
||||
@@ -609,24 +672,37 @@ kubectl -n keycloak-lab describe pod keycloak-0 | grep -A5 Conditions
|
||||
|
||||
Keycloak 은 클러스터 분단을 readiness 로 신고한다. DB 는 UP 인데 클러스터가 DOWN 이고, 쿠버네티스가 그 신고를 받아 처리한다.
|
||||
|
||||
```bash label="[kc-lab-1] Service 뒤에 누가 남았는지 본다"
|
||||
```bash label="[lab host] Service 뒤에 누가 남았는지 본다"
|
||||
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
|
||||
-o custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
|
||||
-o "custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready"
|
||||
```
|
||||
|
||||
실측은 `ready 주소: [10.42.0.35]`, `notReady : [10.42.1.67]` 다(observed). `kubectl get endpoints` 는 v1.33 부터 deprecated 라 경고가 뜨므로 쓰지 않는다.
|
||||
여기도 `-o` 값을 따옴표로 묶는다. `[*]` 가 앞의 `[0]` 과 같은 이유로 글로브다.
|
||||
|
||||
```bash label="[kc-lab-1] 밖에서 정문을 친다"
|
||||
실측은 `ready 주소: [10.42.0.35]`, `notReady : [10.42.1.67]` 다(observed). 2026-09-17 에는 이렇게 나왔다(observed).
|
||||
|
||||
```text
|
||||
NAME ADDR READY
|
||||
keycloak-lxk8h [10.42.0.13],[10.42.1.17] true,false
|
||||
``` `kubectl get endpoints` 는 v1.33 부터 deprecated 라 경고가 뜨므로 쓰지 않는다.
|
||||
|
||||
```bash label="[lab host] 밖에서 정문을 친다"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
|
||||
인증서 단계를 건너뛴 실험대라면 443 을 듣는 것이 없어 여기는 `000` 이다. 그때는 TLS 를 빼고 `Host` 헤더를 실어 엣지에 친다 — 2026-09-17 에 이 형태로 `200` 을 받았다(observed).
|
||||
|
||||
```bash label="[lab host] TLS 없이 같은 것을 잰다"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: auth.hyeonworks.com' http://192.168.122.10/realms/master
|
||||
```
|
||||
|
||||
실측은 정문이 `HTTP 200`, 토큰 발급도 `HTTP 200` 이다(observed). 분단된 노드가 스스로 로드밸런서에서 빠졌고 서비스는 계속됐다. liveness 였다면 재시작을 반복했을 텐데 재시작해도 안 나아지는 문제이므로 readiness 로 격리하는 쪽이 맞는 신호다. 다만 비대칭이라서 살았다 — `keycloak-1` 은 멤버가 하나 줄어든 정상적인 사건이라 Ready 를 유지했고, `keycloak-0` 은 합류 자체를 못 해 DOWN 이 됐다. 양쪽이 동시에 DOWN 이 되는 경로가 있다면 전면 장애이고, 그것이 A-5 의 주제다.
|
||||
|
||||
본 시험은 Service 를 쓰면 안 된다. `keycloak-0` 이 NotReady 라 Service 로 보내면 전부 `keycloak-1` 로 간다. 새 주소로 임시 파드를 다시 띄우고 파드 주소로 직접 친다.
|
||||
|
||||
`keycloak-0` 을 지웠으므로 `$K0` 가 낡았다. 두 주소를 다시 잡고 그 값으로 파드를 띄운다. `$PW` 도 여기서 다시 넣는다 — 앞의 포트 시험 파드에는 안 넣었다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 새 주소를 잡고 탐침 파드를 다시 띄운다"
|
||||
```bash label="[lab host] ① 새 주소를 잡고 탐침 파드를 다시 띄운다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
kubectl -n keycloak-lab run kc-probe --rm -it --restart=Never \
|
||||
@@ -664,7 +740,22 @@ curl -s -w '\n%{http_code}\n' -X POST "http://$K0:8080$TOK" \
|
||||
-d grant_type=refresh_token -d client_id=admin-cli -d "refresh_token=$RT"
|
||||
```
|
||||
|
||||
네 단계를 다 쳤으면 파드 셸에서 나온다. 뒤의 명령은 전부 `[kc-lab-1]` 이고, 나가지 않으면 `kubectl` 도 `psql` 도 없는 `curlimages/curl` 안에서 치게 된다.
|
||||
:::warning
|
||||
|
||||
**[4] 는 응답 본문을 통째로 찍는다.** 분단 중에는 그 요청이 성공하므로 `access_token` 과 `refresh_token` 이 화면에 그대로 나오고, 화면을 갈무리하면 증거 파일에도 그대로 들어간다. 2026-09-17 실행에서 실제로 그렇게 나와 길이만 남기고 가렸다(observed). 판정에 필요한 것은 코드와 `error` 뿐이므로, 갈무리할 생각이면 아래 형태로 친다.
|
||||
|
||||
:::
|
||||
|
||||
```sh label="[탐침 파드] [4] 를 토큰 없이 재는 형태"
|
||||
curl -s -X POST "http://$K0:8080$TOK" \
|
||||
-d grant_type=refresh_token -d client_id=admin-cli -d "refresh_token=$RT" \
|
||||
-w '\n%{http_code}\n' -o /tmp/r
|
||||
sed -n 's/.*"error":"\([^"]*\)".*/error=\1/p' /tmp/r
|
||||
```
|
||||
|
||||
`200` 이면 `error` 줄이 안 나오고, 그 없음이 곧 「로그아웃이 전파되지 않았다」다.
|
||||
|
||||
네 단계를 다 쳤으면 파드 셸에서 나온다. 뒤의 명령은 전부 `[lab host]` 이고, 나가지 않으면 `kubectl` 도 `psql` 도 없는 `curlimages/curl` 안에서 치게 된다.
|
||||
|
||||
```sh label="[탐침 파드] ③ 나온다. --rm 이 파드를 지운다"
|
||||
exit
|
||||
@@ -683,7 +774,7 @@ exit
|
||||
|
||||
[4] 의 200 이 로그아웃이 아예 안 됐다는 뜻인지 확인한다. sid 는 [1] 에서 JWT payload 를 풀어 화면에 찍고 적어 둔 그 값이다. 로그인할 때마다 새로 생기므로 자기 실행의 값을 넣는다.
|
||||
|
||||
```bash label="[kc-lab-1] 그 세션의 DB 행이 남아 있는지 본다"
|
||||
```bash label="[lab host] 그 세션의 DB 행이 남아 있는지 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select user_session_id, offline_flag, last_session_refresh
|
||||
from offline_user_session where user_session_id='{{SID}}'"
|
||||
@@ -699,7 +790,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
(0 rows) ← DB 행은 삭제되었다
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] 세션 캐시 엔트리를 노드별로 본다"
|
||||
```bash label="[lab host] 세션 캐시 엔트리를 노드별로 본다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_statistics_approximate_entries_unique{cache="sessions"}'
|
||||
```
|
||||
@@ -724,12 +815,12 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
|
||||
**목적** — 7800 을 다시 열어 두 노드가 하나의 뷰로 합쳐지게 한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 시각을 남기고 정책을 지운다"
|
||||
```bash label="[lab host] ① 시각을 남기고 정책을 지운다"
|
||||
date '+%H:%M:%S 해제'
|
||||
kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 30초 간격으로 멤버 수를 몇 번 친다"
|
||||
```bash label="[lab host] ② 30초 간격으로 멤버 수를 몇 번 친다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
|
||||
```
|
||||
@@ -755,7 +846,7 @@ networkpolicy.networking.k8s.io "a1-block-jgroups-transport" deleted from keyclo
|
||||
|
||||
**목적** — 재형성이 저절로 일어난 것인지 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] 병합 이벤트 계수기를 다시 읽는다"
|
||||
```bash label="[lab host] 병합 이벤트 계수기를 다시 읽는다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_jgroups_merge3_get_num_merge_events'
|
||||
```
|
||||
|
||||
+71
-51
@@ -45,11 +45,15 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령을 치는 곳이 둘이다. 대부분은 `[kc-lab-1]` 에서 `kubectl` 로 치고, `iptables` 만 노드 자체를 건드리므로 `kc-lab-1` 과 `kc-lab-2` 에 각각 들어가 친다. 코드블록마다 `label` 로 어디서 치는지 붙였다.
|
||||
명령을 치는 곳이 둘이다. 대부분은 `[lab host]` 에서 `kubectl` 로 치고, `iptables` 만 노드 자체를 건드리므로 `kc-lab-1` 과 `kc-lab-2` 에 각각 들어가 친다. 코드블록마다 `label` 로 어디서 치는지 붙였다.
|
||||
|
||||
**`[kc-lab-1]` 라벨이 붙은 블록은 게스트 셸이다.** lab host 에서 `ssh kc-lab-1` 로 들어가서 치고, 끝나면 `exit` 로 나온다. `ssh kc-lab-2 '…'` 한 줄 형태는 lab host 에서 그대로 쳐도 되므로 `[lab host]` 로 두었다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
`kubectl` 에 `sudo` 를 붙이지 않는다. 실험 폴더의 README 가 까닭을 적는다 — `sudo` 를 붙이면 root 환경으로 돌아 그 kubeconfig 를 못 본다. root 홈에는 `~/.kube/config` 가 없으므로 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝나고, 그러면 클러스터가 아니라 누구의 설정 파일을 읽느냐가 문제인데 클러스터를 의심하게 된다.
|
||||
|
||||
```bash label="[kc-lab-1] 두 형태의 차이"
|
||||
```bash label="[lab host] 두 형태의 차이"
|
||||
kubectl -n keycloak-lab get pods # 이렇게
|
||||
sudo kubectl -n keycloak-lab get pods # 이렇게 치면 안 된다
|
||||
```
|
||||
@@ -99,7 +103,7 @@ A-1 은 인터넷 자료의 통념과 어긋난 답을 냈고 그 까닭을 「2
|
||||
|
||||
중간에 그만두려면 두 가지를 되돌린다. `args` 쪽은 이 두 줄이다.
|
||||
|
||||
```bash label="[kc-lab-1] args 를 기본값으로 되돌린다"
|
||||
```bash label="[lab host] args 를 기본값으로 되돌린다"
|
||||
kubectl -n keycloak-lab patch statefulset keycloak --type=json \
|
||||
-p '[{"op":"replace","path":"/spec/template/spec/containers/0/args","value":["start"]}]'
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||||
@@ -111,7 +115,7 @@ kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||||
sudo iptables -t raw -F PREROUTING
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] 반대 노드로 붙는다"
|
||||
```bash label="[lab host] 반대 노드로 붙는다"
|
||||
ssh kc-lab-2
|
||||
```
|
||||
|
||||
@@ -132,7 +136,7 @@ exit
|
||||
|
||||
**목적** — 두 Keycloak 파드가 서로 다른 노드에 있는지 확인하고, 뒤에서 쓸 IP 를 변수에 담는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 노드와 파드를 넓게 본다"
|
||||
```bash label="[lab host] ① 노드와 파드를 넓게 본다"
|
||||
kubectl get nodes
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
@@ -148,7 +152,7 @@ postgres-7b474b88c8-t6rrf 1/1 Running 0 5d 10.42.0.22 kc-lab-1
|
||||
|
||||
`READY` 가 둘 다 `1/1`, `RESTARTS` 가 `0`, 그리고 `NODE` 가 서로 다른지를 본다. 파드 번호와 노드 번호는 어긋난다 — `keycloak-0` 이 `kc-lab-2` 에 있다.
|
||||
|
||||
```bash label="[kc-lab-1] ② IP 를 변수에 담는다"
|
||||
```bash label="[lab host] ② IP 를 변수에 담는다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
echo "$K0 $K1"
|
||||
@@ -166,7 +170,7 @@ echo "$K0 $K1"
|
||||
|
||||
**목적** — 복구할 때 되돌릴 문자열을 확보한다.
|
||||
|
||||
```bash label="[kc-lab-1] 컨테이너 args 를 그대로 찍는다"
|
||||
```bash label="[lab host] 컨테이너 args 를 그대로 찍는다"
|
||||
kubectl -n keycloak-lab get statefulset keycloak \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].args}' ; echo
|
||||
```
|
||||
@@ -185,7 +189,7 @@ kubectl -n keycloak-lab get statefulset keycloak \
|
||||
|
||||
**목적** — persistent 에서 로그인이 DB 행을 만든다는 것을 전환 전에 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] 온라인 세션과 offline token 을 나눠 센다"
|
||||
```bash label="[lab host] 온라인 세션과 offline token 을 나눠 센다"
|
||||
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"
|
||||
```
|
||||
@@ -210,7 +214,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
Keycloak 컨테이너에는 `curl` 도 `wget` 도 없어서 `kubectl exec keycloak-0 -- curl` 은 `exit 127` 로 끝난다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 탐침을 띄우고 Ready 를 기다린다"
|
||||
```bash label="[lab host] ① 탐침을 띄우고 Ready 를 기다린다"
|
||||
kubectl -n keycloak-lab run a7-probe --image=curlimages/curl:8.11.1 \
|
||||
--restart=Never \
|
||||
--env="K0=$K0" --env="K1=$K1" \
|
||||
@@ -222,7 +226,7 @@ kubectl -n keycloak-lab wait --for=condition=Ready pod/a7-probe --timeout=120s
|
||||
|
||||
비밀번호는 명령 치환으로 넘어가므로 터미널에도 셸 히스토리에도 값이 남지 않는다. 존재와 길이만 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 비밀번호의 길이만 센다"
|
||||
```bash label="[lab host] ② 비밀번호의 길이만 센다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
|
||||
```
|
||||
@@ -231,7 +235,7 @@ kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
19
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 탐침 안에 값이 들어갔는지 본다"
|
||||
```bash label="[lab host] ③ 탐침 안에 값이 들어갔는지 본다"
|
||||
kubectl -n keycloak-lab exec a7-probe -- sh -c 'echo "K0=$K0 K1=$K1 PW길이=${#PW}"'
|
||||
```
|
||||
|
||||
@@ -247,7 +251,7 @@ kubectl -n keycloak-lab exec a7-probe -- sh -c 'echo "K0=$K0 K1=$K1 PW길이=${#
|
||||
|
||||
응답을 한 번은 통째로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 로그인 응답 전문을 본다"
|
||||
```bash label="[lab host] ① 로그인 응답 전문을 본다"
|
||||
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 \
|
||||
@@ -261,7 +265,7 @@ kubectl -n keycloak-lab exec a7-probe -- sh -c \
|
||||
|
||||
`expires_in` 이 60 이다. access token 은 60초짜리고 그동안은 서버에 안 물어보므로, 이 실험의 탐침은 access token 이 아니라 refresh 다. refresh 는 노드가 세션 저장소를 실제로 뒤져야 답할 수 있다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 토큰을 파드 안 파일에 담고 길이를 찍는다"
|
||||
```bash label="[lab host] ② 토큰을 파드 안 파일에 담고 길이를 찍는다"
|
||||
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 \
|
||||
@@ -274,7 +278,7 @@ kubectl -n keycloak-lab exec a7-probe -- sh -c \
|
||||
rt 1188 bytes
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 반대 노드에서 그 토큰으로 갱신한다"
|
||||
```bash label="[lab host] ③ 반대 노드에서 그 토큰으로 갱신한다"
|
||||
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" \
|
||||
@@ -296,7 +300,7 @@ kubectl -n keycloak-lab exec a7-probe -- sh -c \
|
||||
|
||||
**목적** — 기능 목록에 이름이 있는지 본다.
|
||||
|
||||
```bash label="[kc-lab-1] 빌드 기능 목록에서 이름을 찾는다"
|
||||
```bash label="[lab host] 빌드 기능 목록에서 이름을 찾는다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kc.sh build --help-all \
|
||||
| tr ',' '\n' | grep -i persistent
|
||||
```
|
||||
@@ -321,7 +325,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kc.sh build --help-
|
||||
|
||||
**목적** — 전환 후 「DB 0건」이 성립할 수 있게 옛 행을 먼저 없앤다.
|
||||
|
||||
```bash label="[kc-lab-1] 온라인·오프라인 세션 행을 전부 지운다"
|
||||
```bash label="[lab host] 온라인·오프라인 세션 행을 전부 지운다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "delete from offline_user_session"
|
||||
```
|
||||
@@ -344,7 +348,7 @@ DELETE 151
|
||||
|
||||
먼저 매니페스트를 편집기로 연다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 저장소의 매니페스트를 연다"
|
||||
```bash label="[lab host] ① 저장소의 매니페스트를 연다"
|
||||
vim deploy/lab/k8s/keycloak-cluster.yaml
|
||||
```
|
||||
|
||||
@@ -357,13 +361,13 @@ args: ["start", "--features-disabled=persistent-user-sessions"]
|
||||
|
||||
고친 파일을 적용한다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 적용한다"
|
||||
```bash label="[lab host] ② 적용한다"
|
||||
kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
|
||||
```
|
||||
|
||||
파일을 안 건드리고 싶으면 patch 를 쓴다.
|
||||
|
||||
```bash label="[kc-lab-1] 파일 대신 patch 로 바꾸는 형태"
|
||||
```bash label="[lab host] 파일 대신 patch 로 바꾸는 형태"
|
||||
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"]}]'
|
||||
@@ -371,7 +375,7 @@ kubectl -n keycloak-lab patch statefulset keycloak --type=json \
|
||||
|
||||
전환 시각을 적고 롤아웃을 기다린다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 전환 시각을 남기고 롤아웃을 기다린다"
|
||||
```bash label="[lab host] ③ 전환 시각을 남기고 롤아웃을 기다린다"
|
||||
date '+%H:%M:%S 전환'
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||||
```
|
||||
@@ -396,7 +400,7 @@ partitioned roll out complete: 2 new pods have been updated...
|
||||
|
||||
**목적** — 선언만 바뀌고 프로세스는 그대로인 상태를 걸러 낸다.
|
||||
|
||||
```bash label="[kc-lab-1] args 와 파드 나이를 함께 본다"
|
||||
```bash label="[lab host] args 와 파드 나이를 함께 본다"
|
||||
kubectl -n keycloak-lab get statefulset keycloak \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].args}' ; echo
|
||||
kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||||
@@ -416,13 +420,13 @@ kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||||
|
||||
IP 가 바뀌었으므로 다시 잡고, 탐침 파드도 지우고 새 IP 로 다시 띄운다. 탐침의 `K0`·`K1` 은 만들 때 고정된 값이라 롤아웃 뒤에는 낡았고, 낡은 주소로 친 curl 은 아무 데도 안 닿는다. 여기서는 아직 파드 안에 지킬 파일이 없으므로 지우고 다시 만들어도 잃을 것이 없다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 롤아웃 뒤 IP 를 다시 잡는다"
|
||||
```bash label="[lab host] ① 롤아웃 뒤 IP 를 다시 잡는다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
echo "$K0 $K1"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 탐침을 지우고 새 IP 로 다시 띄운다"
|
||||
```bash label="[lab host] ② 탐침을 지우고 새 IP 로 다시 띄운다"
|
||||
kubectl -n keycloak-lab delete pod a7-probe --ignore-not-found
|
||||
kubectl -n keycloak-lab run a7-probe --image=curlimages/curl:8.11.1 \
|
||||
--restart=Never \
|
||||
@@ -437,7 +441,7 @@ kubectl -n keycloak-lab wait --for=condition=Ready pod/a7-probe --timeout=120s
|
||||
|
||||
**목적** — args 문자열이 아니라 동작이 바뀐 것을 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 한쪽 노드에만 다섯 번 로그인한다"
|
||||
```bash label="[lab host] ① 한쪽 노드에만 다섯 번 로그인한다"
|
||||
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 \
|
||||
@@ -451,7 +455,7 @@ kubectl -n keycloak-lab exec a7-probe -- sh -c \
|
||||
200 200 200 200 200
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 같은 질의로 DB 행을 다시 센다"
|
||||
```bash label="[lab host] ② 같은 질의로 DB 행을 다시 센다"
|
||||
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"
|
||||
```
|
||||
@@ -473,7 +477,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**목적** — 다음 사람이 이 지표로 판정하지 않도록, 두 모드가 같은 값을 낸다는 것을 눈으로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 한 줄짜리 JSON 을 통째로 본다"
|
||||
```bash label="[lab host] ① 한 줄짜리 JSON 을 통째로 본다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_statistics_approximate_entries_unique'
|
||||
```
|
||||
@@ -486,7 +490,7 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
|
||||
라벨을 보고 나면 읽기 좋게 자른다. 아래 형태는 가이드가 미검증으로 표시한 줄이다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② 캐시 이름과 파드와 값만 세로로 늘어놓는다"
|
||||
```bash label="[lab host] ② 캐시 이름과 파드와 값만 세로로 늘어놓는다"
|
||||
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]'
|
||||
@@ -511,7 +515,7 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
|
||||
**목적** — 겉보기 결과가 persistent 때와 같은지 본다.
|
||||
|
||||
```bash label="[kc-lab-1] 로그인하고 반대 노드에서 갱신한다"
|
||||
```bash label="[lab host] 로그인하고 반대 노드에서 갱신한다"
|
||||
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 \
|
||||
@@ -539,6 +543,20 @@ kubectl -n keycloak-lab exec a7-probe -- sh -c \
|
||||
|
||||
구별하려면 그 경로를 끊어 봐야 하고, 14번이 그것을 한다.
|
||||
|
||||
2026-09-17 에 새 실험대에서 12번과 14번을 이어서 쳤다(observed). 같은 명령 한 벌이 전환 전후로 정반대 답을 냈다.
|
||||
|
||||
```text
|
||||
volatile · 분단 없음 로그인 sid=_szXQIOVaE8_v2-b-frn9npt 교차 노드 refresh 200
|
||||
DB 세션 행 (0 rows)
|
||||
캐시 keycloak-0 sessions 1 · keycloak-1 sessions 0
|
||||
|
||||
volatile · 7800 차단 로그인 sid=9tpo41BVY32uJdYOyv8maA5j 교차 노드 refresh 400
|
||||
로그아웃 400 · 발급 노드 재갱신 400 invalid_grant / Invalid refresh token
|
||||
DB 세션 행 0
|
||||
```
|
||||
|
||||
**persistent 에서는 같은 것을 재면 `200` 이 나왔다.** A-1 에서 7800 을 막고 잰 값이 `200`·`204`·`200` 이었고 여기서는 `400`·`400`·`400` 이다. 그 한 칸이 두 모드의 차이를 전부 말한다 — persistent 는 DB 를 읽고, volatile 은 7800 을 탄다.
|
||||
|
||||
**문제가 생기면** — `400` 이 나오면 `/tmp/rt` 를 다시 안 채웠다. 로그인부터 다시 친다.
|
||||
|
||||
### 13. A-8 을 다시 돌린다 — 롤링 재시작이 곧 로그아웃이다
|
||||
@@ -547,7 +565,7 @@ kubectl -n keycloak-lab exec a7-probe -- sh -c \
|
||||
|
||||
재시작 전에 로그인해서 토큰과 `sid` 를 파드 안에 담는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 토큰을 담고 access token 의 클레임을 편다"
|
||||
```bash label="[lab host] ① 토큰을 담고 access token 의 클레임을 편다"
|
||||
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 \
|
||||
@@ -573,7 +591,7 @@ access token 의 가운데 토막이 클레임이다. 모양은 이렇고 값은
|
||||
|
||||
그다음 재시작한다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 시각을 남기고 롤링 재시작을 건다"
|
||||
```bash label="[lab host] ② 시각을 남기고 롤링 재시작을 건다"
|
||||
date '+%H:%M:%S 재시작'
|
||||
kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||||
@@ -584,15 +602,15 @@ statefulset.apps/keycloak restarted
|
||||
partitioned roll out complete: 2 new pods have been updated...
|
||||
```
|
||||
|
||||
파드 IP 를 다시 잡는다. 탐침은 다시 띄우지 않는다 — `/tmp/rt` 가 같이 사라진다. 그래서 새 IP 를 명령줄에 직접 넘긴다. `$K0` 는 `[kc-lab-1]` 셸의 변수이고 지금 값은 롤아웃 전 것이므로 먼저 다시 잡는다.
|
||||
파드 IP 를 다시 잡는다. 탐침은 다시 띄우지 않는다 — `/tmp/rt` 가 같이 사라진다. 그래서 새 IP 를 명령줄에 직접 넘긴다. `$K0` 는 `[lab host]` 셸의 변수이고 지금 값은 롤아웃 전 것이므로 먼저 다시 잡는다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 롤아웃 뒤 IP 를 다시 잡는다"
|
||||
```bash label="[lab host] ③ 롤아웃 뒤 IP 를 다시 잡는다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
echo "$K0 $K1"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ④ 재시작 전 토큰으로 갱신을 시도한다"
|
||||
```bash label="[lab host] ④ 재시작 전 토큰으로 갱신을 시도한다"
|
||||
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" \
|
||||
@@ -637,7 +655,7 @@ date '+%H:%M:%S 차단'
|
||||
| `kc-lab-1` | `keycloak-1` | `$K1` |
|
||||
| `kc-lab-2` | `keycloak-0` | `$K0` |
|
||||
|
||||
**뒤의 두 줄은 중단 절차처럼 나눠 치면 안 된다.** 거기서는 `ssh kc-lab-2` 로 먼저 붙고 원격 셸에서 쳤지만, 여기는 `$K0` 가 들어간다. `$K0` 는 `[kc-lab-1]` 셸의 변수라 원격 셸에는 없고, 나눠 치면 빈 문자열이 들어가 `-d` 없는 규칙이 걸린다. 큰따옴표가 그 값을 `[kc-lab-1]` 에서 펴서 보내므로 이 두 줄은 한 줄 형태 그대로 친다. 붙어서 치고 싶으면 먼저 `echo "$K0"` 로 값을 읽어 원격 셸에서 IP 를 손으로 넣는다.
|
||||
**뒤의 두 줄은 중단 절차처럼 나눠 치면 안 된다.** 거기서는 `ssh kc-lab-2` 로 먼저 붙고 원격 셸에서 쳤지만, 여기는 `$K0` 가 들어간다. `$K0` 는 `[lab host]` 셸의 변수라 원격 셸에는 없고, 나눠 치면 빈 문자열이 들어가 `-d` 없는 규칙이 걸린다. 큰따옴표가 그 값을 `[lab host]` 에서 펴서 보내므로 이 두 줄은 한 줄 형태 그대로 친다. 붙어서 치고 싶으면 먼저 `echo "$K0"` 로 값을 읽어 원격 셸에서 IP 를 손으로 넣는다.
|
||||
|
||||
NetworkPolicy 대신 `iptables` 를 쓰는 까닭은 A-1 에서 나왔다. NetworkPolicy 는 conntrack 의 ESTABLISHED 를 못 뚫어서 이미 붙어 있는 7800 연결이 계속 산다.
|
||||
|
||||
@@ -670,7 +688,7 @@ Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
|
||||
|
||||
분단이 성립할 때까지 25초 간격으로 몇 번 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 두 노드가 각각 아는 멤버 수를 본다"
|
||||
```bash label="[lab host] ③ 두 노드가 각각 아는 멤버 수를 본다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
|
||||
| tr ',' '\n' | grep -E '"pod":|^"[0-9]'
|
||||
@@ -690,7 +708,7 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
|
||||
split brain 은 DB 한 줄로 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 디스커버리 테이블의 코디네이터를 센다"
|
||||
```bash label="[lab host] ④ 디스커버리 테이블의 코디네이터를 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select name, ip, coord from jgroups_ping order by name"
|
||||
```
|
||||
@@ -706,7 +724,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
대조군을 먼저 재고 시험군을 잰다. 대조군은 같은 노드에서 갱신한다.
|
||||
|
||||
```bash label="[kc-lab-1] ⑤ 대조군 — 로그인한 노드에서 갱신한다"
|
||||
```bash label="[lab host] ⑤ 대조군 — 로그인한 노드에서 갱신한다"
|
||||
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 \
|
||||
@@ -718,7 +736,7 @@ kubectl -n keycloak-lab exec a7-probe -- sh -c \
|
||||
-d "refresh_token=$(cat /tmp/rt)"'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ⑥ 시험군 — 새로 로그인해서 반대 노드에서 갱신한다"
|
||||
```bash label="[lab host] ⑥ 시험군 — 새로 로그인해서 반대 노드에서 갱신한다"
|
||||
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 \
|
||||
@@ -759,9 +777,9 @@ kubectl -n keycloak-lab exec a7-probe -- sh -c \
|
||||
|
||||
내리기 전에 로그인해서 `/tmp/rt` 를 채운다. **12번의 명령을 쓰지 않는다** — 그 블록은 로그인한 다음 곧바로 반대 노드에서 refresh 까지 해서 방금 받은 토큰을 소모한다. refresh token 은 한 번 쓰면 회전하므로 `/tmp/rt` 에는 이미 쓴 값이 남고, DB 를 내린 뒤의 `500` 이 DB 때문인지 재사용 때문인지 구별되지 않는다. 로그인만 하고 끝나는 13번의 ① 을 쓴다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 로그인만 해서 /tmp/rt 를 채운다"
|
||||
```bash label="[lab host] ① 로그인만 해서 /tmp/rt 를 채운다"
|
||||
kubectl -n keycloak-lab exec a7-probe -- sh -c \
|
||||
'curl -s -X POST "http://$K0:8080/realms/master/protocol/openid-connect/token" \
|
||||
'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
|
||||
@@ -769,9 +787,11 @@ kubectl -n keycloak-lab exec a7-probe -- sh -c \
|
||||
| cut -d. -f2 | base64 -d 2>/dev/null; echo'
|
||||
```
|
||||
|
||||
**인용이 세 겹인 것이 여기서도 중요하다.** 작은따옴표 안에 그냥 `$K0` 를 두면 그것을 펴는 것은 파드 안 셸이고, 그 값은 탐침을 만들 때 `--env` 로 **구워 넣은 것**이다. 12번의 `rollout restart` 로 파드 IP 가 바뀐 뒤라 그 값은 낡았다 — 요청이 없는 주소로 나가고, 그때 나오는 실패가 이 절의 예상 결과인 `500` 과 화면에서 같아 보인다. 위 ④ 와 같은 형태로 `[lab host]` 셸의 지금 값을 넘긴다.
|
||||
|
||||
그다음 DB 를 내린다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 시각을 남기고 DB 를 0 replica 로 내린다"
|
||||
```bash label="[lab host] ② 시각을 남기고 DB 를 0 replica 로 내린다"
|
||||
date '+%H:%M:%S 정지'
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=0
|
||||
kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s
|
||||
@@ -781,7 +801,7 @@ kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s
|
||||
|
||||
두 경로를 차례로 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 캐시를 가진 노드에서 refresh"
|
||||
```bash label="[lab host] ③ 캐시를 가진 노드에서 refresh"
|
||||
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" \
|
||||
@@ -789,7 +809,7 @@ kubectl -n keycloak-lab exec a7-probe -- sh -c \
|
||||
-d "refresh_token=$(cat /tmp/rt)"'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ④ 새 로그인"
|
||||
```bash label="[lab host] ④ 새 로그인"
|
||||
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" \
|
||||
@@ -820,7 +840,7 @@ persistent 에서는 순서가 거꾸로였다. 새 로그인이 `500` 이었는
|
||||
|
||||
마지막으로 DB 를 되살린다.
|
||||
|
||||
```bash label="[kc-lab-1] ⑤ DB 를 다시 올린다"
|
||||
```bash label="[lab host] ⑤ DB 를 다시 올린다"
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||||
kubectl -n keycloak-lab rollout status deployment/postgres --timeout=180s
|
||||
```
|
||||
@@ -841,13 +861,13 @@ sudo iptables -t raw -L PREROUTING -n
|
||||
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② DB 파드를 본다"
|
||||
```bash label="[lab host] ② DB 파드를 본다"
|
||||
kubectl -n keycloak-lab get pods -l app=postgres
|
||||
```
|
||||
|
||||
`Running` 이 아니면 `scale deployment/postgres --replicas=1` 을 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ args 를 기본값으로 되돌린다"
|
||||
```bash label="[lab host] ③ args 를 기본값으로 되돌린다"
|
||||
kubectl -n keycloak-lab patch statefulset keycloak --type=json \
|
||||
-p '[{"op":"replace","path":"/spec/template/spec/containers/0/args","value":["start"]}]'
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||||
@@ -855,7 +875,7 @@ kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||||
|
||||
매니페스트를 고쳤다면 파일도 같이 되돌린다. 안 그러면 다음에 `apply` 할 때 volatile 로 다시 간다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 매니페스트의 변경을 확인하고 되돌린다"
|
||||
```bash label="[lab host] ④ 매니페스트의 변경을 확인하고 되돌린다"
|
||||
git diff deploy/lab/k8s/keycloak-cluster.yaml
|
||||
git checkout -- deploy/lab/k8s/keycloak-cluster.yaml
|
||||
```
|
||||
@@ -868,7 +888,7 @@ partitioned roll out complete: 2 new pods have been updated...
|
||||
|
||||
`args` 문자열만 보고 끝내지 않는다. 새 IP 로 탐침을 다시 띄우고 로그인을 한 번 한 다음 DB 행을 센다. 원복도 롤아웃이므로 여기서도 파드 주소가 바뀌었다.
|
||||
|
||||
```bash label="[kc-lab-1] ⑤ 새 IP 로 탐침을 다시 띄운다"
|
||||
```bash label="[lab host] ⑤ 새 IP 로 탐침을 다시 띄운다"
|
||||
kubectl -n keycloak-lab delete pod a7-probe --ignore-not-found
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
@@ -881,14 +901,14 @@ kubectl -n keycloak-lab run a7-probe --image=curlimages/curl:8.11.1 \
|
||||
kubectl -n keycloak-lab wait --for=condition=Ready pod/a7-probe --timeout=120s
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ⑥ 로그인을 한 번 한다"
|
||||
```bash label="[lab host] ⑥ 로그인을 한 번 한다"
|
||||
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"'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ⑦ 로그인 뒤 온라인 세션 행을 센다"
|
||||
```bash label="[lab host] ⑦ 로그인 뒤 온라인 세션 행을 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-tAc "select count(*) from offline_user_session where offline_flag='0'"
|
||||
```
|
||||
@@ -917,7 +937,7 @@ postgres-7b474b88c8-t6rrf 1/1 Running 0 2m8s
|
||||
| 탐침 파드 | `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 label="[kc-lab-1] ⑧ 탐침 파드를 지운다"
|
||||
```bash label="[lab host] ⑧ 탐침 파드를 지운다"
|
||||
kubectl -n keycloak-lab delete pod a7-probe --ignore-not-found
|
||||
```
|
||||
|
||||
|
||||
+77
-50
@@ -41,7 +41,9 @@ PostgreSQL 문장 로깅을 켜고 volatile 상태의 로그인과 refresh 가
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 전부 `[kc-lab-1]` 에서 `kubectl` 로 친다. 이 절차에는 노드 자체를 건드리는 명령이 없어서 `kc-lab-2` 로 들어갈 일이 없다. `kubectl` 에 `sudo` 를 붙이지 않는다 — root 홈에는 `~/.kube/config` 가 없어서 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝난다.
|
||||
명령은 전부 `[lab host]` 에서 `kubectl` 로 친다. 이 절차에는 노드 자체를 건드리는 명령이 없어서 게스트에 들어갈 일이 없다. `kubectl` 에 `sudo` 를 붙이지 않는다 — root 홈에는 `~/.kube/config` 가 없어서 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝난다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
이 편의 시각은 UTC 다. 증거의 `11:18:49` 는 KST 로 `20:18` 이고 같은 순간이다. PostgreSQL 컨테이너가 UTC 로 로그를 찍기 때문이고, 로그 시각과 `date` 를 견줄 때 이걸 잊으면 9시간을 헤맨다.
|
||||
|
||||
@@ -89,7 +91,7 @@ A-7 은 이렇게 끝났다.
|
||||
|
||||
표식을 넣는 방식에서 이 절차가 원 실행과 갈라진다. 원 실행은 표식을 셸 함수로 감쌌다.
|
||||
|
||||
```bash label="[kc-lab-1] 이 실험대는 이렇게 했다 (observed)"
|
||||
```bash label="[lab host] 이 실험대는 이렇게 했다 (observed)"
|
||||
m() { kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||
psql -U keycloak -d keycloak -tAc "select 'MARK_$1'" >/dev/null; }
|
||||
```
|
||||
@@ -98,7 +100,7 @@ m() { kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||
|
||||
따라 하는 사람은 표식을 한 줄씩 손으로 넣는다. 느리지만 그 즉시 보이고, 안 보이면 그 즉시 안다. 아래 절차가 전부 그 형태다.
|
||||
|
||||
```bash label="[kc-lab-1] 따라 하는 사람은 이 형태로 친다"
|
||||
```bash label="[lab host] 따라 하는 사람은 이 형태로 친다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-tAc "select 'MARK_TEST'"
|
||||
```
|
||||
@@ -113,7 +115,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**목적** — 이 절차가 내렸다 올릴 `postgres` 가 지금 있는지 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] 네임스페이스의 파드를 넓게 본다"
|
||||
```bash label="[lab host] 네임스페이스의 파드를 넓게 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
|
||||
@@ -134,7 +136,7 @@ postgres-7b474b88c8-t6rrf 1/1 Running 0 5d 10.42.0.22 kc-lab-1
|
||||
|
||||
**목적** — 지금 쌓이는 로그가 이 실험 것인지 앞 실험 것인지 가른다.
|
||||
|
||||
```bash label="[kc-lab-1] 현재 설정값을 읽는다"
|
||||
```bash label="[lab host] 현재 설정값을 읽는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "show log_statement"
|
||||
```
|
||||
@@ -155,7 +157,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**목적** — 복구에서 되돌릴 문자열을 확보한다.
|
||||
|
||||
```bash label="[kc-lab-1] 컨테이너 args 를 그대로 찍는다"
|
||||
```bash label="[lab host] 컨테이너 args 를 그대로 찍는다"
|
||||
kubectl -n keycloak-lab get statefulset keycloak \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].args}' ; echo
|
||||
```
|
||||
@@ -176,7 +178,7 @@ kubectl -n keycloak-lab get statefulset keycloak \
|
||||
|
||||
Keycloak 컨테이너에는 `curl` 도 `wget` 도 없어서 `kubectl exec keycloak-0 -- curl` 은 `exit 127` 로 끝난다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 탐침을 띄우고 Ready 를 기다린다"
|
||||
```bash label="[lab host] ① 탐침을 띄우고 Ready 를 기다린다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
kubectl -n keycloak-lab run a7a-probe --image=curlimages/curl:8.11.1 \
|
||||
--restart=Never \
|
||||
@@ -189,7 +191,7 @@ kubectl -n keycloak-lab wait --for=condition=Ready pod/a7a-probe --timeout=120s
|
||||
|
||||
비밀번호는 명령 치환으로 넘어가므로 터미널에도 셸 히스토리에도 값이 남지 않는다. 길이만 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 비밀번호의 길이만 센다"
|
||||
```bash label="[lab host] ② 비밀번호의 길이만 센다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
|
||||
```
|
||||
@@ -198,7 +200,7 @@ kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
19
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 탐침 안에 값이 들어갔는지 본다"
|
||||
```bash label="[lab host] ③ 탐침 안에 값이 들어갔는지 본다"
|
||||
kubectl -n keycloak-lab exec a7a-probe -- sh -c 'echo "K0=$K0 PW길이=${#PW}"'
|
||||
```
|
||||
|
||||
@@ -212,7 +214,7 @@ kubectl -n keycloak-lab exec a7a-probe -- sh -c 'echo "K0=$K0 PW길이=${#PW}"'
|
||||
|
||||
**목적** — 켜기 전의 로그를 한 번 봐 두고, 켠 뒤의 소음과 견준다.
|
||||
|
||||
```bash label="[kc-lab-1] 마지막 20줄을 본다"
|
||||
```bash label="[lab host] 마지막 20줄을 본다"
|
||||
kubectl -n keycloak-lab logs deploy/postgres --tail=20
|
||||
```
|
||||
|
||||
@@ -232,12 +234,12 @@ kubectl -n keycloak-lab logs deploy/postgres --tail=20
|
||||
|
||||
되돌리는 명령을 먼저 읽어 둔다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 되돌리는 명령 — 먼저 읽어 둔다"
|
||||
```bash label="[lab host] ① 되돌리는 명령 — 먼저 읽어 둔다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "alter system reset log_statement" -c "select pg_reload_conf()"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 문장 로깅을 켜고 설정을 다시 읽힌다"
|
||||
```bash label="[lab host] ② 문장 로깅을 켜고 설정을 다시 읽힌다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "alter system set log_statement='all'" -c "select pg_reload_conf()"
|
||||
```
|
||||
@@ -254,13 +256,13 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
되돌리는 명령을 먼저 읽어 둔다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 되돌리는 명령 — 먼저 읽어 둔다"
|
||||
```bash label="[lab host] ① 되돌리는 명령 — 먼저 읽어 둔다"
|
||||
kubectl -n keycloak-lab patch statefulset keycloak --type=json \
|
||||
-p '[{"op":"replace","path":"/spec/template/spec/containers/0/args","value":["start"]}]'
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② persistent-user-sessions 를 끈다"
|
||||
```bash label="[lab host] ② persistent-user-sessions 를 끈다"
|
||||
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"]}]'
|
||||
@@ -269,7 +271,7 @@ kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||||
|
||||
파드 IP 가 바뀌었으므로 탐침 파드를 다시 띄운다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 탐침을 지우고 새 IP 로 다시 띄운다"
|
||||
```bash label="[lab host] ③ 탐침을 지우고 새 IP 로 다시 띄운다"
|
||||
kubectl -n keycloak-lab delete pod a7a-probe --ignore-not-found
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
kubectl -n keycloak-lab run a7a-probe --image=curlimages/curl:8.11.1 \
|
||||
@@ -290,7 +292,7 @@ kubectl -n keycloak-lab wait --for=condition=Ready pod/a7a-probe --timeout=120s
|
||||
|
||||
**목적** — DB 가 없는 구간을 만들어 캐시가 무엇을 대신하는지 본다.
|
||||
|
||||
```bash label="[kc-lab-1] DB 를 0 replica 로 내리고 파드가 사라질 때까지 기다린다"
|
||||
```bash label="[lab host] DB 를 0 replica 로 내리고 파드가 사라질 때까지 기다린다"
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=0
|
||||
kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s
|
||||
```
|
||||
@@ -309,7 +311,7 @@ kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s
|
||||
|
||||
**목적** — 설정값과 실제 출력을 둘 다 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 설정값을 읽는다"
|
||||
```bash label="[lab host] ① 설정값을 읽는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "show log_statement"
|
||||
```
|
||||
@@ -320,7 +322,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
all
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 로그가 차는지 본다"
|
||||
```bash label="[lab host] ② 로그가 차는지 본다"
|
||||
kubectl -n keycloak-lab logs deploy/postgres --tail=10
|
||||
```
|
||||
|
||||
@@ -338,12 +340,12 @@ kubectl -n keycloak-lab logs deploy/postgres --tail=10
|
||||
|
||||
**목적** — 구간을 자를 수 있는 상태인지 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 표식을 하나 넣는다"
|
||||
```bash label="[lab host] ① 표식을 하나 넣는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-tAc "select 'MARK_TEST'"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 그 표식이 로그에 있는지 본다"
|
||||
```bash label="[lab host] ② 그 표식이 로그에 있는지 본다"
|
||||
kubectl -n keycloak-lab logs deploy/postgres --tail=5 | grep MARK_TEST
|
||||
```
|
||||
|
||||
@@ -361,7 +363,7 @@ kubectl -n keycloak-lab logs deploy/postgres --tail=5 | grep MARK_TEST
|
||||
|
||||
**목적** — 선언과 동작이 같이 바뀌었는지 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] args · 로그인 응답 코드 · DB 행 수를 이어서 본다"
|
||||
```bash label="[lab host] args · 로그인 응답 코드 · DB 행 수를 이어서 본다"
|
||||
kubectl -n keycloak-lab get statefulset keycloak \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].args}' ; echo
|
||||
kubectl -n keycloak-lab exec a7a-probe -- sh -c \
|
||||
@@ -387,7 +389,7 @@ volatile 전환 확인
|
||||
|
||||
문장 로그가 지금 요청을 잡고 있는지도 본다.
|
||||
|
||||
```bash label="[kc-lab-1] 최근 60초의 로그 끝을 본다"
|
||||
```bash label="[lab host] 최근 60초의 로그 끝을 본다"
|
||||
kubectl -n keycloak-lab logs deploy/postgres --since=60s | tail -20
|
||||
```
|
||||
|
||||
@@ -401,7 +403,7 @@ kubectl -n keycloak-lab logs deploy/postgres --since=60s | tail -20
|
||||
|
||||
**목적** — 로그인 한 번이 DB 에 무엇을 보내는지 센다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 표식 · 로그인 · 표식을 차례로 친다"
|
||||
```bash label="[lab host] ① 표식 · 로그인 · 표식을 차례로 친다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-tAc "select 'MARK_LOGIN_START'"
|
||||
kubectl -n keycloak-lab exec a7a-probe -- sh -c \
|
||||
@@ -418,7 +420,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
rt 1188 bytes
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 로그를 파일로 받아 구간을 자른다"
|
||||
```bash label="[lab host] ② 로그를 파일로 받아 구간을 자른다"
|
||||
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
|
||||
```
|
||||
@@ -439,7 +441,7 @@ awk '/MARK_LOGIN_START/,/MARK_LOGIN_END/' /tmp/pg.log | grep -v JGROUPS_PING
|
||||
|
||||
**목적** — A-7 의 가설이 지목한 테이블이 실제로 나오는지 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 표식 · refresh · 표식을 차례로 친다"
|
||||
```bash label="[lab host] ① 표식 · refresh · 표식을 차례로 친다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-tAc "select 'MARK_REFRESH_START'"
|
||||
kubectl -n keycloak-lab exec a7a-probe -- sh -c \
|
||||
@@ -451,7 +453,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-tAc "select 'MARK_REFRESH_END'"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 그 구간을 자른다"
|
||||
```bash label="[lab host] ② 그 구간을 자른다"
|
||||
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
|
||||
```
|
||||
@@ -473,7 +475,7 @@ awk '/MARK_REFRESH_START/,/MARK_REFRESH_END/' /tmp/pg.log | grep -v JGROUPS_PING
|
||||
|
||||
가설이 지목한 테이블이 정말 없는지 직접 센다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 그 구간에서 REVOKED_TOKEN 을 센다"
|
||||
```bash label="[lab host] ③ 그 구간에서 REVOKED_TOKEN 을 센다"
|
||||
awk '/MARK_REFRESH_START/,/MARK_REFRESH_END/' /tmp/pg.log | grep -ci revoked_token
|
||||
```
|
||||
|
||||
@@ -492,7 +494,7 @@ refresh 는 새 access token 을 만든다. 그 토큰에 어떤 스코프를
|
||||
|
||||
그 UUID 가 어느 클라이언트인지 궁금하면 물어본다. UUID 는 렐름을 만들 때 정해지므로 실험대마다 다르다. ②가 자른 구간의 `parameters` 줄에 있는 `$1` 값을 그대로 옮겨 넣는다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 그 UUID 가 어느 클라이언트인지 묻는다"
|
||||
```bash label="[lab host] ④ 그 UUID 가 어느 클라이언트인지 묻는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select id, client_id from client where id='{{CLIENT_UUID}}'"
|
||||
```
|
||||
@@ -507,7 +509,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**목적** — 첫 refresh 가 캐시를 채우고 이후로는 DB 를 보지 않는다는 것을 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 표식을 사이사이에 넣으며 refresh 를 돈다"
|
||||
```bash label="[lab host] ① 표식을 사이사이에 넣으며 refresh 를 돈다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-tAc "select 'MARK_R1'"
|
||||
kubectl -n keycloak-lab exec a7a-probe -- sh -c \
|
||||
@@ -522,7 +524,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
같은 모양으로 `MARK_R3` 과 `MARK_R_END` 까지 두 번 더 한다. 매번 `/tmp/rt` 를 다시 채운다 — refresh token 은 회전하고, 옛것을 계속 쓰면 나오는 오류가 무효화 때문인지 재사용 때문인지 구별되지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 표식 넷 사이를 통째로 자른다"
|
||||
```bash label="[lab host] ② 표식 넷 사이를 통째로 자른다"
|
||||
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
|
||||
```
|
||||
@@ -542,6 +544,16 @@ awk '/MARK_R1/,/MARK_R_END/' /tmp/pg.log | grep -v JGROUPS_PING
|
||||
|
||||
**문제가 생기면** — `400 Session not active` 가 나오면 옛 refresh token 을 재사용했다. 매번 `/tmp/rt` 를 갱신한다.
|
||||
|
||||
**세 재현의 2026-09-17 실측**(observed) — 같은 설정에서 캐시 온도만 달리해 셋을 이어서 쳤다.
|
||||
|
||||
| 재현 | 만든 상태 | 쳤을 때 |
|
||||
|---|---|---|
|
||||
| A | 재시작 직후, 로그인 0회, DB 정지 | 로그인이 `400 unauthorized_client` |
|
||||
| B | 재시작 뒤 로그인 1회로 캐시를 덥히고 DB 정지 | refresh 가 `500 unknown_error` |
|
||||
| C | DB 를 다시 켜고 같은 refresh | `200` |
|
||||
|
||||
A 의 `400` 은 `invalid_grant` 가 아니라 `unauthorized_client` 다 — 세션이 아니라 **클라이언트를 못 찾은 것**이고, 그래서 이 셋을 안 갈라 놓고 재면 A-7 의 결론을 잘못 읽는다.
|
||||
|
||||
### 15. 재현 A — 완전 냉시동이면 로그인부터 400 이다
|
||||
|
||||
**목적** — 캐시가 전부 빈 상태에서 DB 를 내렸을 때의 답을 잰다.
|
||||
@@ -557,7 +569,7 @@ awk '/MARK_R1/,/MARK_R_END/' /tmp/pg.log | grep -v JGROUPS_PING
|
||||
|
||||
이 재시작을 건너뛰면 세 상태가 하나로 뭉개진다. 이미 더워진 캐시에서 계속 재게 되므로 A 와 B 를 재도 C 의 답이 나오고, 「A-7 이 틀렸다」는 엉뚱한 결론에 이른다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 재시작하고 곧바로 DB 를 내린다"
|
||||
```bash label="[lab host] ① 재시작하고 곧바로 DB 를 내린다"
|
||||
kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=0
|
||||
@@ -568,7 +580,7 @@ kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s
|
||||
|
||||
탐침의 `K0` 는 만들 때 고정된 값이라 재시작 뒤에는 낡았다. 지우고 새 IP 로 다시 만든다. 이 블록을 건너뛰면 뒤의 curl 이 없는 주소로 가고, 그 침묵을 「DB 가 없어서 실패」로 읽게 된다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 탐침을 지우고 새 IP 로 다시 띄운다"
|
||||
```bash label="[lab host] ② 탐침을 지우고 새 IP 로 다시 띄운다"
|
||||
kubectl -n keycloak-lab delete pod a7a-probe --ignore-not-found
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
kubectl -n keycloak-lab run a7a-probe --image=curlimages/curl:8.11.1 \
|
||||
@@ -579,7 +591,7 @@ kubectl -n keycloak-lab run a7a-probe --image=curlimages/curl:8.11.1 \
|
||||
kubectl -n keycloak-lab wait --for=condition=Ready pod/a7a-probe --timeout=120s
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 로그인을 본문과 함께 본다"
|
||||
```bash label="[lab host] ③ 로그인을 본문과 함께 본다"
|
||||
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" \
|
||||
@@ -596,11 +608,26 @@ kubectl -n keycloak-lab exec a7a-probe -- sh -c \
|
||||
|
||||
`unauthorized_client` 이고 `invalid_grant` 가 아니다. 세션 문제가 아니라 클라이언트를 못 찾았다. 왜인지는 Keycloak 로그가 직접 말한다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 실패한 SQL 을 Keycloak 로그에서 뽑는다"
|
||||
kubectl -n keycloak-lab logs keycloak-0 --tail=150 \
|
||||
| grep -oE 'JDBC exception executing SQL \[[^]]*\] \[[^]]*\]'
|
||||
```bash label="[lab host] ④ 실패한 SQL 을 Keycloak 로그에서 뽑는다"
|
||||
kubectl -n keycloak-lab logs keycloak-0 --tail=400 \
|
||||
| grep -oE 'KC-SERVICES0015[^[]*\[[^]]*\]'
|
||||
```
|
||||
|
||||
**이 형태로 넓혔다.** 원래 적힌 `grep -oE 'JDBC exception executing SQL …'` 은 **속 예외가 그 문구일 때만** 잡는데, 속 예외는 DB 를 언제 끊었느냐에 따라 둘로 갈린다. 2026-09-17 에는 `--tail=150` 과 그 패턴으로 **한 줄도 안 나왔다**(observed).
|
||||
|
||||
```text
|
||||
끊는 중에 걸린 요청 GenericJDBCException: JDBC exception executing SQL [FATAL: terminating connection due to administrator command]
|
||||
이미 사라진 뒤의 요청 JDBCConnectionException: Unable to acquire JDBC Connection [Connection to postgres:5432 refused. …]
|
||||
```
|
||||
|
||||
둘 다 앞머리가 `KC-SERVICES0015: Unexpected error when authenticating client` 이므로 그것으로 거는 편이 안전하다. 2026-09-17 실측은 뒤엣것이었다(observed).
|
||||
|
||||
```text
|
||||
KC-SERVICES0015: Unexpected error when authenticating client: org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection [Connection to postgres:5432 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.]
|
||||
```
|
||||
|
||||
`--tail` 도 늘렸다. 재시작 직후에는 기동 로그가 길어서 150 줄로는 이 줄까지 못 거슬러 올라간다.
|
||||
|
||||
```text
|
||||
ERROR [org.keycloak.services] KC-SERVICES0015: Unexpected error when
|
||||
authenticating client: org.hibernate.exception.GenericJDBCException:
|
||||
@@ -617,7 +644,7 @@ kubectl -n keycloak-lab logs keycloak-0 --tail=150 \
|
||||
|
||||
**목적** — A-7 이 본 그 조건을 그대로 만든다.
|
||||
|
||||
```bash label="[kc-lab-1] ① DB 를 살리고 다시 재시작한다"
|
||||
```bash label="[lab host] ① DB 를 살리고 다시 재시작한다"
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||||
kubectl -n keycloak-lab rollout status deployment/postgres --timeout=180s
|
||||
kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
||||
@@ -626,7 +653,7 @@ kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||||
|
||||
탐침을 새 IP 로 다시 띄운 뒤 로그인 한 번만 한다. 15번과 같은 이유로 여기서도 탐침을 다시 만든다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 탐침을 지우고 새 IP 로 다시 띄운다"
|
||||
```bash label="[lab host] ② 탐침을 지우고 새 IP 로 다시 띄운다"
|
||||
kubectl -n keycloak-lab delete pod a7a-probe --ignore-not-found
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
kubectl -n keycloak-lab run a7a-probe --image=curlimages/curl:8.11.1 \
|
||||
@@ -637,7 +664,7 @@ kubectl -n keycloak-lab run a7a-probe --image=curlimages/curl:8.11.1 \
|
||||
kubectl -n keycloak-lab wait --for=condition=Ready pod/a7a-probe --timeout=120s
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 로그인 한 번으로 캐시를 절반만 데운다"
|
||||
```bash label="[lab host] ③ 로그인 한 번으로 캐시를 절반만 데운다"
|
||||
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 \
|
||||
@@ -648,7 +675,7 @@ kubectl -n keycloak-lab exec a7a-probe -- sh -c \
|
||||
|
||||
여기서 refresh 를 하면 재현 C 가 된다. 8번의 DB 정지를 친 다음에 refresh 한다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ DB 가 없는 상태에서 refresh 한다"
|
||||
```bash label="[lab host] ④ DB 가 없는 상태에서 refresh 한다"
|
||||
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" \
|
||||
@@ -682,7 +709,7 @@ kubectl -n keycloak-lab exec a7a-probe -- sh -c \
|
||||
|
||||
DB 를 살리고, 재시작하고, 탐침을 새로 만들고, 로그인하고, refresh 를 3회 미리 돌린 뒤 DB 를 내린다. 앞 절들의 명령을 그대로 다시 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ① DB 를 살리고 다시 재시작한다"
|
||||
```bash label="[lab host] ① DB 를 살리고 다시 재시작한다"
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||||
kubectl -n keycloak-lab rollout status deployment/postgres --timeout=180s
|
||||
kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
||||
@@ -691,7 +718,7 @@ kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||||
|
||||
파드가 새로 떴으므로 `K0` 가 낡았다. 탐침도 그 값을 `--env` 로 박아 뒀으니 같이 다시 만든다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 탐침을 지우고 새 IP 로 다시 띄운다"
|
||||
```bash label="[lab host] ② 탐침을 지우고 새 IP 로 다시 띄운다"
|
||||
kubectl -n keycloak-lab delete pod a7a-probe --ignore-not-found
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
kubectl -n keycloak-lab run a7a-probe --image=curlimages/curl:8.11.1 \
|
||||
@@ -704,7 +731,7 @@ kubectl -n keycloak-lab wait --for=condition=Ready pod/a7a-probe --timeout=120s
|
||||
|
||||
새 탐침에는 `/tmp/rt` 가 없다. 16번의 ③ 과 같은 명령으로 다시 만든다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 로그인해서 /tmp/rt 를 새로 만든다"
|
||||
```bash label="[lab host] ③ 로그인해서 /tmp/rt 를 새로 만든다"
|
||||
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 \
|
||||
@@ -717,12 +744,12 @@ kubectl -n keycloak-lab exec a7a-probe -- sh -c \
|
||||
|
||||
그 세 번을 돌렸다고 보고, 8번과 같은 명령으로 DB 를 내린다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ DB 를 0 replica 로 내리고 파드가 사라질 때까지 기다린다"
|
||||
```bash label="[lab host] ④ DB 를 0 replica 로 내리고 파드가 사라질 때까지 기다린다"
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=0
|
||||
kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=90s
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ⑤ 로그인과 refresh 를 이어서 친다"
|
||||
```bash label="[lab host] ⑤ 로그인과 refresh 를 이어서 친다"
|
||||
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" \
|
||||
@@ -765,14 +792,14 @@ persistent 기본값에는 이 조건부성이 없다. 세션 자체를 DB 에
|
||||
|
||||
셋을 순서대로 되돌린다. DB 가 살아 있어야 나머지가 된다.
|
||||
|
||||
```bash label="[kc-lab-1] ① DB 를 올리고 Ready 까지 기다린다"
|
||||
```bash label="[lab host] ① DB 를 올리고 Ready 까지 기다린다"
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||||
kubectl -n keycloak-lab wait --for=condition=Ready pod -l app=postgres --timeout=180s
|
||||
```
|
||||
|
||||
문장 로깅을 끈다. 잊으면 다음 실험이 전부 오염된다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 문장 로깅을 끄고 값을 다시 읽는다"
|
||||
```bash label="[lab host] ② 문장 로깅을 끄고 값을 다시 읽는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "alter system reset log_statement" -c "select pg_reload_conf()"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
@@ -787,7 +814,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
왜 급한가 — A-3 은 수백 건의 로그인을 최대한 빨리 돈다. `log_statement='all'` 이면 로그인 하나에 SQL 열 몇 줄씩 쌓이고, 로그가 폭주하고 디스크 입출력이 늘어 크래시 타이밍 자체가 달라진다. 다음 실험의 측정값이 이 설정 때문에 바뀐다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ args 를 기본값으로 되돌린다"
|
||||
```bash label="[lab host] ③ args 를 기본값으로 되돌린다"
|
||||
kubectl -n keycloak-lab patch statefulset keycloak --type=json \
|
||||
-p '[{"op":"replace","path":"/spec/template/spec/containers/0/args","value":["start"]}]'
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||||
@@ -795,7 +822,7 @@ kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
|
||||
|
||||
`args` 문자열만 보고 끝내지 않는다. 탐침을 새 IP 로 띄우고 로그인을 한 번 한 다음 행을 센다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 로그인 뒤 온라인 세션 행을 센다"
|
||||
```bash label="[lab host] ④ 로그인 뒤 온라인 세션 행을 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-tAc "select count(*) from offline_user_session where offline_flag='0'"
|
||||
```
|
||||
@@ -814,7 +841,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
| 임시 파일 | `ls /tmp/pg.log` | 지워도 된다 |
|
||||
| 밖 | `curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master` | `200` |
|
||||
|
||||
```bash label="[kc-lab-1] ⑤ 탐침과 임시 파일을 치운다"
|
||||
```bash label="[lab host] ⑤ 탐침과 임시 파일을 치운다"
|
||||
kubectl -n keycloak-lab delete pod a7a-probe --ignore-not-found
|
||||
rm -f /tmp/pg.log
|
||||
```
|
||||
|
||||
+51
-30
@@ -41,7 +41,9 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 전부 `[kc-lab-1]` 에서 `kubectl` 로 친다. 노드 자체를 건드리는 명령이 없어서 `kc-lab-2` 로 들어갈 일이 없다. `kubectl` 에 `sudo` 를 붙이지 않는다 — root 홈에는 `~/.kube/config` 가 없어서 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝난다.
|
||||
명령은 전부 `[lab host]` 에서 `kubectl` 로 친다. 노드 자체를 건드리는 명령이 없어서 게스트에 들어갈 일이 없다. `kubectl` 에 `sudo` 를 붙이지 않는다 — root 홈에는 `~/.kube/config` 가 없어서 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝난다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
터미널은 둘을 연다. 하나는 가용성 감시용이라 루프가 도는 동안 붙잡혀 있고, 하나는 재시작과 관찰용이다.
|
||||
|
||||
@@ -86,7 +88,7 @@ A-0 은 「세션의 진실은 PostgreSQL 에 있고 Infinispan 캐시는 사본
|
||||
|
||||
정말 되돌려야 하면 이 명령이 있다. 다만 중간에 `rollout status` 를 `Ctrl-C` 로 끊어도 롤아웃 자체는 계속 진행되므로 끝날 때까지 두는 편이 낫다.
|
||||
|
||||
```bash label="[kc-lab-1] 직전 리비전으로 되돌린다"
|
||||
```bash label="[lab host] 직전 리비전으로 되돌린다"
|
||||
kubectl -n keycloak-lab rollout undo statefulset/keycloak
|
||||
```
|
||||
|
||||
@@ -100,7 +102,7 @@ kubectl -n keycloak-lab rollout undo statefulset/keycloak
|
||||
|
||||
**목적** — 재시작 전의 `AGE` 를 확보하고 replica 가 2 인지 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] 파드를 노드와 함께 넓게 본다"
|
||||
```bash label="[lab host] 파드를 노드와 함께 넓게 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
|
||||
@@ -119,7 +121,7 @@ postgres-7b474b88c8-t6rrf 1/1 Running 0 5d 10.42.0.22 kc-lab-1
|
||||
|
||||
**문제가 생기면** — `keycloak` 파드가 하나뿐이면 이 절차의 가용성 측정은 성립하지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] 두 파드 IP 를 변수에 담는다"
|
||||
```bash label="[lab host] 두 파드 IP 를 변수에 담는다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
echo "$K0 $K1"
|
||||
@@ -129,7 +131,7 @@ echo "$K0 $K1"
|
||||
|
||||
**목적** — `persistent-user-sessions` 가 켜져 있는 상태에서 재는지 본다.
|
||||
|
||||
```bash label="[kc-lab-1] 컨테이너 args 를 그대로 찍는다"
|
||||
```bash label="[lab host] 컨테이너 args 를 그대로 찍는다"
|
||||
kubectl -n keycloak-lab get statefulset keycloak \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].args}' ; echo
|
||||
```
|
||||
@@ -148,7 +150,7 @@ kubectl -n keycloak-lab get statefulset keycloak \
|
||||
|
||||
**목적** — 재시작 후에 견줄 값을 확보한다.
|
||||
|
||||
```bash label="[kc-lab-1] 온라인 세션과 offline token 을 나눠 센다"
|
||||
```bash label="[lab host] 온라인 세션과 offline token 을 나눠 센다"
|
||||
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"
|
||||
```
|
||||
@@ -174,7 +176,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
└─ 단독 탐침 파드의 /tmp → StatefulSet 과 무관하게 산다 ★
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ① 탐침을 띄우고 Ready 를 기다린다"
|
||||
```bash label="[lab host] ① 탐침을 띄우고 Ready 를 기다린다"
|
||||
kubectl -n keycloak-lab run a8-probe --image=curlimages/curl:8.11.1 \
|
||||
--restart=Never \
|
||||
--env="K0=$K0" --env="K1=$K1" \
|
||||
@@ -186,7 +188,7 @@ kubectl -n keycloak-lab wait --for=condition=Ready pod/a8-probe --timeout=120s
|
||||
|
||||
비밀번호는 명령 치환으로 넘어가므로 터미널에도 셸 히스토리에도 값이 남지 않는다. 길이만 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 비밀번호의 길이만 센다"
|
||||
```bash label="[lab host] ② 비밀번호의 길이만 센다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
|
||||
```
|
||||
@@ -195,7 +197,7 @@ kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
19
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 탐침 안에 값이 들어갔는지 본다"
|
||||
```bash label="[lab host] ③ 탐침 안에 값이 들어갔는지 본다"
|
||||
kubectl -n keycloak-lab exec a8-probe -- sh -c 'echo "K0=$K0 K1=$K1 PW길이=${#PW}"'
|
||||
```
|
||||
|
||||
@@ -205,7 +207,7 @@ kubectl -n keycloak-lab exec a8-probe -- sh -c 'echo "K0=$K0 K1=$K1 PW길이=${#
|
||||
|
||||
**문제가 생기면** — `PW길이=0` 이면 `--env` 가 빈 값을 받았다. 파드를 지우고 ① 부터 다시 한다. `--rm` 이 없는 상주 파드라 지우지 않으면 같은 이름이 그대로 있어 ① 이 `AlreadyExists` 로 거절되고, 이 절차를 두 번째 칠 때도 같은 곳에서 걸린다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 탐침을 지우고 ① 로 돌아간다"
|
||||
```bash label="[lab host] ④ 탐침을 지우고 ① 로 돌아간다"
|
||||
kubectl -n keycloak-lab delete pod a8-probe --ignore-not-found
|
||||
```
|
||||
|
||||
@@ -215,7 +217,7 @@ kubectl -n keycloak-lab delete pod a8-probe --ignore-not-found
|
||||
|
||||
이 단계에 이 실험의 함정이 있다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 로그인해서 refresh token 과 sid 를 파일로 남긴다"
|
||||
```bash label="[lab host] ① 로그인해서 refresh token 과 sid 를 파일로 남긴다"
|
||||
kubectl -n keycloak-lab exec a8-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 \
|
||||
@@ -246,7 +248,7 @@ kubectl -n keycloak-lab exec a8-probe -- sh -c \
|
||||
|
||||
못 미더우면 파일을 직접 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 파일 크기와 앞 40바이트를 본다"
|
||||
```bash label="[lab host] ② 파일 크기와 앞 40바이트를 본다"
|
||||
kubectl -n keycloak-lab exec a8-probe -- ls -l /tmp/tok /tmp/rt /tmp/sid
|
||||
kubectl -n keycloak-lab exec a8-probe -- head -c 40 /tmp/rt ; echo
|
||||
```
|
||||
@@ -264,7 +266,7 @@ eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldU
|
||||
|
||||
**목적** — 뒤의 `200` 이 무엇과 견준 값인지 확보한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 같은 노드에서 갱신해 본다"
|
||||
```bash label="[lab host] ① 같은 노드에서 갱신해 본다"
|
||||
kubectl -n keycloak-lab exec a8-probe -- sh -c \
|
||||
'curl -s -o /dev/null -w "%{http_code}\n" -X POST \
|
||||
"http://$K0:8080/realms/master/protocol/openid-connect/token" \
|
||||
@@ -280,7 +282,7 @@ kubectl -n keycloak-lab exec a8-probe -- sh -c \
|
||||
|
||||
이 refresh 로 토큰이 회전했다. `/tmp/rt` 의 값은 이제 이미 쓴 토큰이라 다시 채워야 하고, 안 채우면 뒤의 `400` 이 재시작 때문인지 재사용 때문인지 구별되지 않는다. 5번의 ① 과 같은 명령을 그대로 다시 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 다시 로그인해서 두 파일을 새로 만든다"
|
||||
```bash label="[lab host] ② 다시 로그인해서 두 파일을 새로 만든다"
|
||||
kubectl -n keycloak-lab exec a8-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 \
|
||||
@@ -302,11 +304,11 @@ kubectl -n keycloak-lab exec a8-probe -- sh -c \
|
||||
|
||||
원 가이드의 질의는 `sid` 를 셸 치환으로 집어넣어 `psql -c` 문자열 안에 `kubectl exec` 이 한 번 더 들어간다. 따라 하는 사람은 방금 적어 둔 `sid` 를 그대로 친다 — 앞 명령이 이미 그 값을 화면에 보여 줬고, 명령 하나가 한 가지 일만 한다. 이 두 단계 형태는 이 실험대에서 치지 않았다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ① sid 를 화면에서 읽는다"
|
||||
```bash label="[lab host] ① sid 를 화면에서 읽는다"
|
||||
kubectl -n keycloak-lab exec a8-probe -- cat /tmp/sid
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 읽은 값을 그대로 넣어 행을 찾는다"
|
||||
```bash label="[lab host] ② 읽은 값을 그대로 넣어 행을 찾는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select user_session_id, created_on, last_session_refresh from offline_user_session
|
||||
where offline_flag='0' and user_session_id='{{SID}}'"
|
||||
@@ -333,7 +335,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**목적** — 재시작 후 0 이 되는 값을 먼저 확보한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 한 줄짜리 JSON 을 통째로 본다"
|
||||
```bash label="[lab host] ① 한 줄짜리 JSON 을 통째로 본다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
|
||||
```
|
||||
@@ -348,7 +350,7 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
|
||||
라벨을 보고 나면 읽기 좋게 자른다. 아래 형태는 가이드가 미검증으로 표시한 줄이다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드와 값만 세로로 늘어놓는다"
|
||||
```bash label="[lab host] ② 파드와 값만 세로로 늘어놓는다"
|
||||
kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
|
||||
| tr ',' '\n' | grep -E '"pod":|^"[0-9]'
|
||||
@@ -381,21 +383,21 @@ echo
|
||||
|
||||
첫 번째 터미널에서 재시작한다. `rollout restart` 는 바로 돌아오고, 파드 교체는 그 뒤에 백그라운드로 진행된다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 시각을 남기고 롤링 재시작을 건다"
|
||||
```bash label="[lab host] ② 시각을 남기고 롤링 재시작을 건다"
|
||||
date '+%H:%M:%S 재시작'
|
||||
kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
||||
```
|
||||
|
||||
**엔드포인트는 여기서 봐야 보인다.** 파드가 서비스에서 빠졌다 돌아오는 것은 롤아웃이 도는 동안에만 나타나고, 끝난 뒤에 치면 ready 주소가 늘 둘로 나온다. 두 번째 터미널은 ① 의 루프에 붙잡혀 있으므로 이 터미널에서 몇 번 반복해서 친다. 찍힌 것을 어떻게 읽는지는 15번에서 적는다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 롤아웃이 도는 동안 엔드포인트를 몇 번 본다"
|
||||
```bash label="[lab host] ③ 롤아웃이 도는 동안 엔드포인트를 몇 번 본다"
|
||||
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
|
||||
-o custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
|
||||
-o "custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready"
|
||||
```
|
||||
|
||||
그다음 롤아웃이 끝날 때까지 기다린다. 이 명령은 끝날 때까지 터미널을 붙잡는다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 롤아웃이 끝날 때까지 기다린다"
|
||||
```bash label="[lab host] ④ 롤아웃이 끝날 때까지 기다린다"
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=420s
|
||||
```
|
||||
|
||||
@@ -426,7 +428,7 @@ Waiting for 1 pods to be ready...
|
||||
|
||||
**목적** — 「세션이 살아남았다」가 의미를 갖는 조건을 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] 파드 나이와 재시작 카운터를 본다"
|
||||
```bash label="[lab host] 파드 나이와 재시작 카운터를 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||||
```
|
||||
|
||||
@@ -446,7 +448,7 @@ keycloak-1 1/1 Running 0 66s
|
||||
|
||||
파드 IP 가 바뀌었으므로 다시 잡는다. 탐침 파드는 다시 띄우지 않는다 — `/tmp/rt` 와 `/tmp/sid` 가 같이 사라진다. 탐침 안의 `K0` 환경변수는 낡았으므로 새 IP 를 명령줄로 넘긴다.
|
||||
|
||||
```bash label="[kc-lab-1] 새 파드 IP 를 다시 잡는다"
|
||||
```bash label="[lab host] 새 파드 IP 를 다시 잡는다"
|
||||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
echo "$K0 $K1"
|
||||
@@ -507,7 +509,7 @@ echo
|
||||
|
||||
셸 인용이 세 겹이 되는 형태이고, 가이드는 여기에 다른 형태를 제시하지 않는다. 탐침을 다시 띄우면 토큰이 사라지기 때문이다.
|
||||
|
||||
```bash label="[kc-lab-1] 재시작 전 토큰으로 갱신을 시도한다"
|
||||
```bash label="[lab host] 재시작 전 토큰으로 갱신을 시도한다"
|
||||
kubectl -n keycloak-lab exec a8-probe -- sh -c \
|
||||
'curl -s -w "\n%{http_code}\n" -X POST \
|
||||
"http://'"$K0"':8080/realms/master/protocol/openid-connect/token" \
|
||||
@@ -529,13 +531,32 @@ kubectl -n keycloak-lab exec a8-probe -- sh -c \
|
||||
|
||||
**문제가 생기면** — 아무 데도 안 닿으면 파드 IP 가 바뀐 것을 명령에 반영하지 않았다. 10번의 IP 잡기를 다시 한다.
|
||||
|
||||
**2026-09-17 실측**(observed) — 롤링 재시작 전후로 같은 토큰을 썼다.
|
||||
|
||||
```text
|
||||
재시작 전 대조군 200
|
||||
재시작 (파드 둘 교체) AGE 31s · 63s · IP 10.42.1.47→.49, 10.42.0.20→.21
|
||||
재시작 뒤 같은 refresh 200 · session_state 가 같은 sid
|
||||
DB 행 created_on 1789625835 그대로 · last_session_refresh 1789625835 → 1789625952
|
||||
캐시 재시작 전후 모두 keycloak-0/1 sessions 0
|
||||
클러스터 다시 2
|
||||
```
|
||||
|
||||
**`created_on` 은 그대로이고 `last_session_refresh` 만 117초 뒤로 갔다.** 새로 만든 세션이 아니라 **남아 있던 행을 새 파드가 읽어서 갱신한 것**이고, 그것이 이 편의 결론이다.
|
||||
|
||||
**탐침 안의 `$K0` 는 재시작 뒤에 낡는다.** 2026-09-17 에 낡은 값으로 쳐서 `000` 을 받았다(observed) — 문서가 미리 적어 둔 그 함정이다. 새 IP 를 명령줄로 넘겨 다시 쳤다.
|
||||
|
||||
```text
|
||||
새 K0=10.42.1.49 (탐침 안의 낡은 값=10.42.1.47)
|
||||
```
|
||||
|
||||
### 13. DB 행의 두 시각을 견준다
|
||||
|
||||
**목적** — 응답 코드만이 아니라 쓰기까지 정상인지 본다.
|
||||
|
||||
적어 둔 `sid` 를 넣어 7번의 ② 와 같은 질의를 다시 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 같은 행을 다시 찾는다"
|
||||
```bash label="[lab host] ① 같은 행을 다시 찾는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select user_session_id, created_on, last_session_refresh from offline_user_session
|
||||
where offline_flag='0' and user_session_id='{{SID}}'"
|
||||
@@ -562,7 +583,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
두 값은 유닉스 시각(초)이라 사람이 읽는 형태로 보려면 이렇게 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 두 유닉스 시각을 사람이 읽는 형태로 바꾼다"
|
||||
```bash label="[lab host] ② 두 유닉스 시각을 사람이 읽는 형태로 바꾼다"
|
||||
date -d @1788495513 ; date -d @1788495577
|
||||
```
|
||||
|
||||
@@ -572,7 +593,7 @@ date -d @1788495513 ; date -d @1788495577
|
||||
|
||||
전체 세션 수도 함께 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 온라인 세션 전체를 센다"
|
||||
```bash label="[lab host] ③ 온라인 세션 전체를 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-tAc "select count(*) from offline_user_session where offline_flag='0'"
|
||||
```
|
||||
@@ -587,7 +608,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**목적** — 재시작이 무엇을 지우고 무엇을 남겼는지 가른다.
|
||||
|
||||
```bash label="[kc-lab-1] 캐시 엔트리 수와 클러스터 크기를 이어서 본다"
|
||||
```bash label="[lab host] 캐시 엔트리 수와 클러스터 크기를 이어서 본다"
|
||||
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]'
|
||||
@@ -650,7 +671,7 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
|
||||
주입이 정상 작업이었으므로 되돌릴 것이 없다. 정리만 한다.
|
||||
|
||||
```bash label="[kc-lab-1] 탐침 파드를 지운다"
|
||||
```bash label="[lab host] 탐침 파드를 지운다"
|
||||
kubectl -n keycloak-lab delete pod a8-probe --ignore-not-found
|
||||
```
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"revision": "9465582b5d1630eb4ae7c4e078021486919bf6b6",
|
||||
"verified": "반입할 때 적어 둔 source/.source-revision 이 이 커밋이고 저장소에 실재한다 — 「chore: 실행 환경 구성 문서 추가 및 수정」, 2026-09-10. **전에 이 칸은 cdac9b81 이었고 그것은 틀렸다** (2026-09-17 대조) — 그 커밋은 2026-09-04 이고 반입본 306개 가운데 docs/guides/** 28개가 거기에 아예 없다. 가이드는 그 엿새 뒤 6f6ab86 에서 들어왔다. **그런데 반입한 바이트는 이 커밋과도 같지 않다** — 9465582b 와 같은 것은 276개이고 29개가 다르다. 같은 306개를 저장소의 **작업 트리**와 견주면 297개가 같다. HEAD 에서 200 커밋을 거슬러 전수 대조했을 때 가장 가까운 6f6ab86 도 28개가 어긋났다. **맞는 커밋은 없다** — 반입은 커밋이 아니라 **그 시점의 작업 트리**(미커밋 수정이 있던 상태)에서 떠 온 것이다. 지금도 저장소는 그 파일들을 M 으로 낸다. 작업 트리와 남은 차이 8개 가운데 5개가 그 M 목록에 있고(반입 뒤 저장소가 더 고쳤다), deploy/lab/host/nginx-keycloak-lab.conf 는 저장소에서 deploy/lab/edge/ 로 옮겨져 반입본에만 남았다. **이 커밋은 「반입 시점의 HEAD」라는 뜻이지 「반입한 바이트가 이것이다」가 아니다**"
|
||||
},
|
||||
"ssotSha256": "986918aa05f1a7143ba6a6e30d1eed0e748f0daa8bb70aaf32c56f0f88e56620",
|
||||
"ssotSha256": "f3916ea7c24798627a7e75e5f859af25e29ff404d218816c032e9356f0bf9dc8",
|
||||
"sourceRevision": "keycloak-session-lab@2026-09",
|
||||
"generatedAt": "2026-09-17",
|
||||
"candidateScope": {
|
||||
|
||||
+61
-27
@@ -37,7 +37,9 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
기계가 셋이다. 위조 요청을 보내는 `curl` 은 어디서 쳐도 되고 밖에서 치는 편이 공격자 관점에 가깝다. `kubectl` 은 `[kc-lab-1]` 에서 치고 `sudo` 를 붙이지 않는다. **랩 호스트(`test-server`)로 넘어가는 것은 nginx 설정을 만지는 단계 하나뿐이고**, 그 블록만 `[test-server]` 라벨이 붙어 있다. 5절의 클레임 반영은 브라우저 콘솔에서 잰다 — oauth2-proxy 세션 쿠키가 `HttpOnly` 라 `curl` 로 로그인 상태를 재현할 수 없다.
|
||||
기계가 셋이다. 위조 요청을 보내는 `curl` 은 어디서 쳐도 되고 밖에서 치는 편이 공격자 관점에 가깝다. `kubectl` 은 `[lab host]` 에서 치고 `sudo` 를 붙이지 않는다. **랩 호스트(`test-server`)로 넘어가는 것은 nginx 설정을 만지는 단계 하나뿐이고**, 그 블록만 `[test-server]` 라벨이 붙어 있다. 5절의 클레임 반영은 브라우저 콘솔에서 잰다 — oauth2-proxy 세션 쿠키가 `HttpOnly` 라 `curl` 로 로그인 상태를 재현할 수 없다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -86,7 +88,7 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
그 `HttpOnly` 는 짐작이 아니라 기동 로그에 적혀 있다.
|
||||
|
||||
```bash label="[kc-lab-1] 쿠키 속성을 기동 로그에서 읽는다"
|
||||
```bash label="[lab host] 쿠키 속성을 기동 로그에서 읽는다"
|
||||
kubectl -n keycloak-lab logs -l app=oauth2-proxy | grep -i 'Cookie settings' | head -1
|
||||
```
|
||||
|
||||
@@ -109,14 +111,14 @@ kubectl -n keycloak-lab logs -l app=oauth2-proxy | grep -i 'Cookie settings' | h
|
||||
|
||||
되돌리기는 셋이고 셋 다 먼저 읽어 둔다.
|
||||
|
||||
```bash label="[kc-lab-1] 중간에 그만둘 때 ① Ingress 를 돌려준다"
|
||||
```bash label="[lab host] 중간에 그만둘 때 ① Ingress 를 돌려준다"
|
||||
kubectl -n keycloak-lab delete ingress oauth2-proxy
|
||||
kubectl apply -f /tmp/grafana-ingress-backup.yaml
|
||||
```
|
||||
|
||||
`$USER_ID` 는 주입 5절 ③ 에서 잡는 값이라 여기를 먼저 읽는 지금은 비어 있다. 실제로 칠 일이 생기는 것은 그 절을 친 뒤이고, 1~3 절만 하고 그만두는 사람은 email 을 바꾼 적이 없으니 이 줄 자체가 필요 없다.
|
||||
|
||||
```bash label="[kc-lab-1] 중간에 그만둘 때 ② IdP 의 email 을 되돌린다 — $USER_ID 를 잡은 뒤에 친다"
|
||||
```bash label="[lab host] 중간에 그만둘 때 ② IdP 의 email 을 되돌린다 — $USER_ID 를 잡은 뒤에 친다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
update users/$USER_ID -r keycloak-patterns -s email=labuser@example.com
|
||||
```
|
||||
@@ -140,7 +142,7 @@ sudo nginx -t && sudo systemctl reload nginx
|
||||
|
||||
**무엇을 보는가** — 같은 호스트를 잡고 있는 Ingress 목록.
|
||||
|
||||
```bash label="[kc-lab-1] ① Ingress 를 전부 본다"
|
||||
```bash label="[lab host] ① Ingress 를 전부 본다"
|
||||
kubectl get ingress -A
|
||||
```
|
||||
|
||||
@@ -156,7 +158,7 @@ observability grafana traefik app2.hyeonworks.com 80
|
||||
|
||||
`app1` 이 두 줄이다. 같은 호스트에 Ingress 가 둘이고 경로로 갈린다. 어느 경로가 어디로 가는지는 눈으로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② echo 의 경로 규칙을 본다"
|
||||
```bash label="[lab host] ② echo 의 경로 규칙을 본다"
|
||||
kubectl -n header-lab describe ingress echo | grep -A5 Rules
|
||||
```
|
||||
|
||||
@@ -193,16 +195,30 @@ curl -s https://app1.hyeonworks.com/api/echo
|
||||
|
||||
**이 값이 뜻하는 것** — `headers` 의 값이 전부 배열이다. HTTP 가 같은 이름의 헤더를 여러 번 허용하기 때문이고, 동명 헤더를 두 개 보냈을 때 무엇이 도착했는지도 이 배열이 말해 준다. `x-forwarded-proto` 가 `https` 인 것은 nginx 가 `proxy_set_header` 로 설정한 헤더라서다. `scheme` · `secure` · `serverName` 은 Keycloak 이 `iss` 클레임과 리다이렉트를 만들 때 쓰는 값들이다.
|
||||
|
||||
걸러 볼 때는 `grep -o` 를 쓴다. 가이드가 이 줄을 미검증으로 표시했다(unknown).
|
||||
걸러 볼 때는 `grep -o` 를 쓴다. **2026-09-17 에 쳐 보니 이 형태는 한 줄도 못 잡는다**(observed).
|
||||
|
||||
```bash label="[밖에서] 한 헤더만 뽑아 본다"
|
||||
```bash label="[밖에서] 한 헤더만 뽑아 본다 — 이 형태는 안 걸린다"
|
||||
curl -s https://app1.hyeonworks.com/api/echo | grep -o '"x-forwarded-proto":\[[^]]*\]'
|
||||
```
|
||||
|
||||
`echo` 가 응답을 **들여쓰고 콜론 양옆에 공백을 넣어** 찍기 때문이다. 위 실측은 줄바꿈을 없애고 옮겨 적은 것이라 그 공백이 안 보인다. 실제 화면은 이렇다(observed).
|
||||
|
||||
```text
|
||||
"x-forwarded-proto":["https"]
|
||||
"x-forwarded-proto" : [ "http" ],
|
||||
```
|
||||
|
||||
콜론과 대괄호 안쪽의 공백을 허용한다.
|
||||
|
||||
```bash label="[밖에서] 공백을 허용한 형태"
|
||||
curl -s https://app1.hyeonworks.com/api/echo | grep -o '"x-forwarded-proto" *: *\[[^]]*\]'
|
||||
```
|
||||
|
||||
```text
|
||||
"x-forwarded-proto" : [ "http" ]
|
||||
```
|
||||
|
||||
**아래 3절의 `grep -o '"x-auth-request[^]]*\]'` 는 그대로 써도 된다.** 이름 뒤에 바로 `:` 를 요구하지 않아서 공백이 있어도 걸린다. 콜론을 붙여 쓴 형태만 고치면 된다.
|
||||
|
||||
**`tr ',' '\n' | grep` 은 여기서 쓰면 안 된다.** 값 배열이 `["admin","editor"]` 처럼 쉼표를 품고 있어서 배열이 두 줄로 잘린다. 첫 줄만 보고 「하나만 도착했다」로 읽게 되는데, 이 절차에서 가장 조심할 오독이다. `grep -o '…\[[^]]*\]'` 는 대괄호 안을 통째로 뽑는다.
|
||||
|
||||
### 3. 대조군 — 아무것도 안 붙였을 때 무엇이 도착하나
|
||||
@@ -213,7 +229,7 @@ curl -s https://app1.hyeonworks.com/api/echo | grep -o '"x-forwarded-proto":\[[^
|
||||
curl -s https://app1.hyeonworks.com/api/echo | grep -o '"x-auth-request[^]]*\]'
|
||||
```
|
||||
|
||||
**어디를 보나** — 아무것도 안 나와야 한다.
|
||||
**어디를 보나** — 아무것도 안 나와야 한다. 2026-09-17 에도 아무것도 안 나왔다(observed).
|
||||
|
||||
**이 값이 뜻하는 것** — `x-auth-request-*` 는 엣지가 붙이는 헤더인데 `app1` 앞에는 oauth2-proxy 가 없으므로 지금은 없다. 이 칸이 비어 있는 것이 대조군이다. 주입 뒤 여기에 값이 나타나면 그건 내가 보낸 것이 도착한 것이고, 이 확인을 건너뛰면 원래 있던 것과 내가 넣은 것이 구별되지 않는다.
|
||||
|
||||
@@ -255,14 +271,20 @@ nginx 는 자기가 `proxy_set_header` 로 설정한 헤더만 덮어쓴다. 설
|
||||
|
||||
**목적** — 같은 이름의 헤더 둘이 도착 시점에 어떻게 보이는지 만든다.
|
||||
|
||||
① 같은 헤더를 값만 달리해 두 번 붙인다. 가이드가 미검증으로 표시했다(unknown) — 원래 실행은 스크립트가 응답을 정리했고 아래는 같은 값을 `grep` 으로 뽑는 형태다.
|
||||
① 같은 헤더를 값만 달리해 두 번 붙인다. **2026-09-17 에 쳐서 확인했다**(observed) — 원래 실행은 스크립트가 응답을 정리했고 아래는 같은 값을 `grep` 으로 뽑는 형태다. 콜론 양옆 공백을 허용해야 걸린다.
|
||||
|
||||
```bash label="[밖에서] 동명 헤더 두 개를 보낸다"
|
||||
curl -s -H 'X-Auth-Request-Roles: admin' -H 'X-Auth-Request-Roles: editor' \
|
||||
https://app1.hyeonworks.com/api/echo | grep -o '"x-auth-request-roles":\[[^]]*\]'
|
||||
https://app1.hyeonworks.com/api/echo | grep -o '"x-auth-request-roles" *: *\[[^]]*\]'
|
||||
```
|
||||
|
||||
**예상 결과** — 대괄호 안에 값이 둘 들어온다. 판정은 관찰 절에서 한다.
|
||||
**예상 결과** — 대괄호 안에 값이 둘 들어온다. 2026-09-17 실측이다(observed).
|
||||
|
||||
```text
|
||||
"x-auth-request-roles" : [ "admin", "editor" ]
|
||||
```
|
||||
|
||||
판정은 관찰 절에서 한다.
|
||||
|
||||
**왜 필요한가** — 덮어쓰는지, 합치는지, 통과시키는지 셋 중 어느 것인지가 여기서 갈린다.
|
||||
|
||||
@@ -276,14 +298,19 @@ curl -s -H 'X-Auth-Request-Roles: admin' -H 'X-Auth-Request-Roles: editor' \
|
||||
|
||||
```bash label="[밖에서] 쉼표 구분과 값 안 쉼표를 각각 보낸다"
|
||||
curl -s -H 'X-Auth-Request-Roles: admin,editor,viewer' \
|
||||
https://app1.hyeonworks.com/api/echo | grep -o '"x-auth-request-roles":\[[^]]*\]'
|
||||
https://app1.hyeonworks.com/api/echo | grep -o '"x-auth-request-roles" *: *\[[^]]*\]'
|
||||
curl -s -H 'X-Auth-Request-Roles: role-with,comma' \
|
||||
https://app1.hyeonworks.com/api/echo | grep -o '"x-auth-request-roles":\[[^]]*\]'
|
||||
https://app1.hyeonworks.com/api/echo | grep -o '"x-auth-request-roles" *: *\[[^]]*\]'
|
||||
```
|
||||
|
||||
**예상 결과** — 두 줄이 같은 모양으로 온다.
|
||||
**예상 결과** — 두 줄이 같은 모양으로 온다. 2026-09-17 실측이다(observed).
|
||||
|
||||
**왜 필요한가** — role 이름에 쉼표가 들어갈 수 있다면 쉼표로 자르는 방식이 성립하지 않는다.
|
||||
```text
|
||||
"x-auth-request-roles" : [ "admin,editor,viewer" ]
|
||||
"x-auth-request-roles" : [ "role-with,comma" ]
|
||||
```
|
||||
|
||||
**왜 필요한가** — role 이름에 쉼표가 들어갈 수 있다면 쉼표로 자르는 방식이 성립하지 않는다. 위 두 줄이 도착 시점에 구별되지 않는다는 것이 그 근거다.
|
||||
|
||||
**문제가 생기면** — 대괄호째 뽑았는지 다시 본다.
|
||||
|
||||
@@ -328,7 +355,14 @@ curl -s \
|
||||
https://app1.hyeonworks.com/api/echo
|
||||
```
|
||||
|
||||
**예상 결과** — echo 앱이 응답을 돌려준다. 무엇이 돌아왔는지는 주입 검증에서 읽는다.
|
||||
**예상 결과** — echo 앱이 응답을 돌려준다. 무엇이 돌아왔는지는 주입 검증에서 읽는다. 2026-09-17 에 쳐 보니 위조한 두 줄이 그대로 도착했다(observed).
|
||||
|
||||
```text
|
||||
"x-auth-request-email" : [ "attacker@evil.invalid" ]
|
||||
"x-auth-request-user" : [ "attacker" ]
|
||||
```
|
||||
|
||||
대조군(3절)에서는 그 칸이 비어 있었으므로, 이 두 줄은 **내가 보낸 것이 엣지를 지나 앱까지 닿았다**는 뜻이다.
|
||||
|
||||
**왜 필요한가** — 여기까지가 요청만으로 되는 부분이다. 설치한 것도 받은 권한도 없다.
|
||||
|
||||
@@ -340,7 +374,7 @@ curl -s \
|
||||
|
||||
① 백업이 먼저다. 파일이 생겼는지 줄 수로 확인한 뒤에 원본을 지운다.
|
||||
|
||||
```bash label="[kc-lab-1] ① Grafana Ingress 를 백업하고 프록시를 올린다"
|
||||
```bash label="[lab host] ① Grafana Ingress 를 백업하고 프록시를 올린다"
|
||||
kubectl -n observability get ingress grafana -o yaml > /tmp/grafana-ingress-backup.yaml
|
||||
wc -l /tmp/grafana-ingress-backup.yaml
|
||||
kubectl -n observability delete ingress grafana
|
||||
@@ -354,7 +388,7 @@ kubectl -n keycloak-lab rollout status deployment/oauth2-proxy --timeout=180s
|
||||
|
||||
③ IdP 의 email 을 바꾸고 변경 시각을 UTC 로 남긴다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ IdP 의 email 을 바꾸고 시각을 남긴다"
|
||||
```bash label="[lab host] ③ IdP 의 email 을 바꾸고 시각을 남긴다"
|
||||
USER_ID=$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get users -r keycloak-patterns -q username=labuser \
|
||||
--fields id --format csv --noquotes | tail -1)
|
||||
@@ -429,7 +463,7 @@ done
|
||||
|
||||
**둘째 주입은 IdP 쪽이 정말 바뀌었는지와 세션이 그대로인지를 같이 본다.** 바뀌지 않은 것을 「반영 안 됨」으로 읽지 않으려면 반드시 본다.
|
||||
|
||||
```bash label="[kc-lab-1] IdP 의 email 을 다시 읽는다"
|
||||
```bash label="[lab host] IdP 의 email 을 다시 읽는다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get users/$UID -r keycloak-patterns --fields email
|
||||
```
|
||||
@@ -450,7 +484,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
IdP 값은 바뀌었고 세션은 하나다. 이 두 줄이 있어야 다음 절의 옛 값을 「반영 안 됨」이라고 말할 수 있다. 세션 목록은 지우기 전에 항상 먼저 본다.
|
||||
|
||||
```bash label="[kc-lab-1] oauth2-proxy 세션 키를 본다"
|
||||
```bash label="[lab host] oauth2-proxy 세션 키를 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '_oauth2_proxy-*'
|
||||
```
|
||||
|
||||
@@ -472,7 +506,7 @@ kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '_oauth2
|
||||
도착: ['admin', 'editor'] ← ★ 둘 다 도착. 덮어쓰지도 합치지도 않는다
|
||||
```
|
||||
|
||||
`curl` 로 직접 보면 `"x-auth-request-roles":["admin","editor"]` 로 보인다. 셋 중 어느 것도 아니었다.
|
||||
`curl` 로 직접 보면 `"x-auth-request-roles" : [ "admin", "editor" ]` 로 보인다. 셋 중 어느 것도 아니었다.
|
||||
|
||||
| 가설 | 도착했을 모양 | 실제 |
|
||||
|---|---|---|
|
||||
@@ -630,7 +664,7 @@ D 층의 「106초」와 이 「약 107초」는 같은 왜곡을 두 번 쟀다
|
||||
|
||||
세션을 지우고 재인증시키면 새 값이 온다. 지우기 전에 목록을 본다. 가이드가 두 번째 줄을 미검증으로 표시했다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] 세션을 보고 지운다"
|
||||
```bash label="[lab host] 세션을 보고 지운다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '_oauth2_proxy-*'
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '_oauth2_proxy-*' \
|
||||
| xargs -r kubectl -n keycloak-lab exec deploy/redis -- redis-cli del
|
||||
@@ -694,7 +728,7 @@ D-4a 가 같은 판정법을 인증서 갱신에 쓴다.
|
||||
|
||||
① 사용자 id 를 다시 잡고 값을 되돌린 뒤 같은 명령으로 읽는다.
|
||||
|
||||
```bash label="[kc-lab-1] email 을 되돌리고 다시 읽는다"
|
||||
```bash label="[lab host] email 을 되돌리고 다시 읽는다"
|
||||
USER_ID=$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get users -r keycloak-patterns -q username=labuser \
|
||||
--fields id --format csv --noquotes | tail -1)
|
||||
@@ -720,14 +754,14 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
① oauth2-proxy 것을 먼저 지우고 Grafana 것을 올린다.
|
||||
|
||||
```bash label="[kc-lab-1] ① Ingress 를 돌려준다"
|
||||
```bash label="[lab host] ① Ingress 를 돌려준다"
|
||||
kubectl -n keycloak-lab delete ingress oauth2-proxy
|
||||
kubectl apply -f /tmp/grafana-ingress-backup.yaml
|
||||
```
|
||||
|
||||
② 하나만 남았는지 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] ② app2 를 잡고 있는 Ingress 를 센다"
|
||||
```bash label="[lab host] ② app2 를 잡고 있는 Ingress 를 센다"
|
||||
kubectl get ingress -A | grep app2
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://app2.hyeonworks.com/
|
||||
```
|
||||
|
||||
+47
-26
@@ -41,7 +41,9 @@ oauth2-proxy 의 cookie secret 을 A 에서 B 로 바꿨을 때 로그인해 있
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
기계가 둘이고 표시가 둘이다. `kubectl` 과 `redis-cli` 는 `[kc-lab-1]` 에서 치고, 앞단 nginx 를 건너뛰고 Traefik 을 직접 두드리는 `curl` 과 `sudo` 는 `[test-server]` 에서 친다. 앞단 nginx 가 그 호스트에 있고 `192.168.122.11:80` 으로 넘겨주므로, 호스트에서 그 주소를 바로 치면 nginx 를 건너뛴다.
|
||||
기계가 둘이고 표시가 둘이다. `kubectl` 과 `redis-cli` 는 `[lab host]` 에서 치고, 앞단 nginx 를 건너뛰고 Traefik 을 직접 두드리는 `curl` 과 `sudo` 는 `[test-server]` 에서 친다. 앞단 nginx 가 그 호스트에 있고 `192.168.122.11:80` 으로 넘겨주므로, 호스트에서 그 주소를 바로 치면 nginx 를 건너뛴다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
브라우저도 필요하다. 쿠키가 `HttpOnly` 이고 OIDC(OpenID Connect, OAuth2 위에 신원 확인을 얹은 규격) 흐름을 폼까지 걸어야 세션이 생긴다. `curl` 로 완주하려던 시도는 실패했다.
|
||||
|
||||
@@ -87,11 +89,22 @@ B-6 에서 Keycloak 은 두 키를 동시에 들고 무중단으로 회전했다
|
||||
- B-0 이 끝나 있어야 한다. Redis 를 거기서 띄우고 `redis.keycloak-lab.svc:6379` 로 떠 있다.
|
||||
- 브라우저가 있어야 한다.
|
||||
|
||||
**`oauth2-proxy` 는 인증서가 서기 전에는 못 뜬다.** 기동할 때 OIDC 디스커버리를 먼저 하는데 그 주소가 `https://auth.hyeonworks.com/...` 이라, 인증서 단계를 안 끝낸 실험대에서는 `CrashLoopBackOff` 로 끝난다. 2026-09-17 에 그대로 겪었다(observed).
|
||||
|
||||
```text
|
||||
[provider.go:55] Performing OIDC Discovery...
|
||||
[main.go:59] ERROR: Failed to initialise OAuth2 Proxy: ... error while discovery OIDC configuration:
|
||||
Get "https://auth.hyeonworks.com/realms/keycloak-patterns/.well-known/openid-configuration":
|
||||
dial tcp 100.83.212.4:443: connect: connection refused
|
||||
```
|
||||
|
||||
`oauth2-proxy-secrets` 가 있고 이미지도 받아지는데 기동 자체가 안 된다 — **이 편의 전제는 배포가 아니라 TLS 다.**
|
||||
|
||||
**이건 남의 도메인을 빌리고 남의 세션을 끊는 실험이다.** 둘을 건드린다. 인증서가 `auth` · `app1` · `app2` 세 이름만 덮어서 네 번째 이름을 못 만들기 때문에 Grafana 의 Ingress 를 잠시 내리고 `app2` 를 빌린다. 그리고 secret 을 바꾸면 그때 로그인해 있던 사람의 쿠키가 전부 무효가 된다.
|
||||
|
||||
되돌리기는 둘이고 먼저 읽어 둔다.
|
||||
|
||||
```bash label="[kc-lab-1] ① secret 참조를 A 로 되돌린다"
|
||||
```bash label="[lab host] ① secret 참조를 A 로 되돌린다"
|
||||
kubectl -n keycloak-lab patch deployment oauth2-proxy --type=json \
|
||||
-p '[{"op":"replace",
|
||||
"path":"/spec/template/spec/containers/0/env/1/valueFrom/secretKeyRef/key",
|
||||
@@ -100,7 +113,7 @@ kubectl -n keycloak-lab patch deployment oauth2-proxy --type=json \
|
||||
|
||||
①의 `env/1` 은 매니페스트의 env 배열 순서에 달린 숫자다. 중간에 멈춰서 되돌리는 사람은 주입 2 절 ①의 `env[*].name` 을 먼저 쳐서 `OAUTH2_PROXY_COOKIE_SECRET` 이 0 부터 세어 몇 번째인지 보고, 두 번째가 아니면 위 경로의 `1` 을 그 숫자로 바꾼다. 숫자가 틀리면 클라이언트 비밀을 쿠키 secret 으로 덮어쓴다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 빌린 Ingress 를 걷고 Grafana 것을 올린다"
|
||||
```bash label="[lab host] ② 빌린 Ingress 를 걷고 Grafana 것을 올린다"
|
||||
kubectl -n keycloak-lab delete ingress oauth2-proxy
|
||||
kubectl apply -f ~/grafana-ingress-backup.yaml
|
||||
```
|
||||
@@ -125,7 +138,7 @@ curl -sI https://app2.hyeonworks.com/ | head -3
|
||||
|
||||
**②를 치기 전에 읽는다.** 셸은 `>` 를 kubectl 보다 먼저 처리한다. `~/grafana-ingress-backup.yaml` 은 kubectl 이 돌기도 전에 0바이트가 되고, `get` 이 실패하면 앞서 떠 둔 백업이 그때 없어진다. 뒤따르는 `wc -l` 과 `grep -c` 는 이미 비어 버린 파일을 센다. 그래서 이 절을 두 번째로 치는 사람은 — B-4 로 app2 를 먼저 빌렸거나 실험을 중간에 다시 시작했다면 — `wc -l ~/grafana-ingress-backup.yaml` 을 먼저 쳐서 쓸 만한 백업을 이미 갖고 있는지 보고, 갖고 있으면 ②를 건너뛴다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 백업을 뜨고 내용이 있는지 센다"
|
||||
```bash label="[lab host] ② 백업을 뜨고 내용이 있는지 센다"
|
||||
kubectl -n observability get ingress grafana -o yaml > ~/grafana-ingress-backup.yaml
|
||||
wc -l ~/grafana-ingress-backup.yaml
|
||||
grep -c 'app2.hyeonworks.com' ~/grafana-ingress-backup.yaml
|
||||
@@ -143,11 +156,11 @@ grep -c 'app2.hyeonworks.com' ~/grafana-ingress-backup.yaml
|
||||
|
||||
**행동** — 내리고, 올리고, 롤아웃이 끝날 때까지 기다린다. ②의 `deploy/lab/k8s/b7-oauth2-proxy.yaml` 은 저장소 체크아웃의 루트에서 푸는 상대 경로다. 체크아웃을 어디에 뒀는지는 가이드에 없으므로(unknown), 그 경로가 풀리는 디렉터리로 옮긴 다음 ②를 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ① Grafana Ingress 를 내린다"
|
||||
```bash label="[lab host] ① Grafana Ingress 를 내린다"
|
||||
kubectl -n observability delete ingress grafana
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② oauth2-proxy 를 배포한다"
|
||||
```bash label="[lab host] ② oauth2-proxy 를 배포한다"
|
||||
kubectl apply -f deploy/lab/k8s/b7-oauth2-proxy.yaml
|
||||
kubectl -n keycloak-lab rollout status deploy/oauth2-proxy --timeout=180s
|
||||
```
|
||||
@@ -175,7 +188,7 @@ deployment "oauth2-proxy" successfully rolled out
|
||||
|
||||
**무엇을 보는가** — 파드 수와 배치.
|
||||
|
||||
```bash label="[kc-lab-1] 파드 배치를 본다"
|
||||
```bash label="[lab host] 파드 배치를 본다"
|
||||
kubectl -n keycloak-lab get pods -l app=oauth2-proxy -o wide
|
||||
```
|
||||
|
||||
@@ -230,7 +243,7 @@ curl -H "Host: app2.hyeonworks.com" http://192.168.122.11/ → 302
|
||||
|
||||
**이 값이 뜻하는 것** — Traefik 직접은 정상이므로 502 를 내는 것은 그 앞의 nginx 이고, 502 는 쿠키를 설정하는 응답에서만 났다. oauth2-proxy 는 기본적으로 세션 전체를 쿠키에 담는데 그 `Set-Cookie` 가 nginx 의 `proxy_buffer_size` 를 넘겼다. B-4 에서 본 헤더 크기 절벽이 이번에는 응답 쪽에서 나타났다 — 거기서는 요청 헤더가 8KB 에서 400 이 됐고, 여기서는 응답 헤더가 프록시 버퍼를 넘겨 502 가 됐다. 해결은 세션을 Redis 로 옮기는 것이고 매니페스트에 이미 들어 있다.
|
||||
|
||||
```bash label="[kc-lab-1] 세션 저장소가 Redis 인지 인자에서 본다"
|
||||
```bash label="[lab host] 세션 저장소가 Redis 인지 인자에서 본다"
|
||||
kubectl -n keycloak-lab get deploy oauth2-proxy \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].args}' | tr ',' '\n' | grep -i session
|
||||
```
|
||||
@@ -261,7 +274,7 @@ sudo: a password is required
|
||||
|
||||
**무엇을 보는가** — 회전 대상이 준비되어 있는지. 값은 찍지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① Secret 의 키 이름만 뽑는다"
|
||||
```bash label="[lab host] ① Secret 의 키 이름만 뽑는다"
|
||||
kubectl -n keycloak-lab get secret oauth2-proxy-secrets \
|
||||
-o jsonpath='{.data}' | tr ',' '\n' | grep -o '"[A-Z_]*"'
|
||||
```
|
||||
@@ -276,7 +289,7 @@ kubectl -n keycloak-lab get secret oauth2-proxy-secrets \
|
||||
|
||||
길이도 본다. 가이드가 아래 두 줄을 미검증으로 표시했다(unknown) — 원래 실행 기록에 이 명령의 출력이 없다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 두 cookie secret 의 길이만 센다 (unknown)"
|
||||
```bash label="[lab host] ② 두 cookie secret 의 길이만 센다 (unknown)"
|
||||
kubectl -n keycloak-lab get secret oauth2-proxy-secrets \
|
||||
-o jsonpath='{.data.COOKIE_SECRET_A}' | base64 -d | wc -c
|
||||
kubectl -n keycloak-lab get secret oauth2-proxy-secrets \
|
||||
@@ -308,7 +321,7 @@ kubectl -n keycloak-lab get secret oauth2-proxy-secrets \
|
||||
|
||||
**무엇을 보는가** — 한 번은 통째로 본 다음 접두사로 좁힌다.
|
||||
|
||||
```bash label="[kc-lab-1] ① Redis 전체를 본다"
|
||||
```bash label="[lab host] ① Redis 전체를 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '*'
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||||
```
|
||||
@@ -324,7 +337,7 @@ _oauth2_proxy-b26111fbd1fdab3ae2182e287001b02a
|
||||
|
||||
`dbsize` 는 2 인데 세션은 하나다. `b5:pvc` 는 B-5 가 남긴 키이고 이 실험과 무관하다. 이 Redis 는 이 실험 전용이 아니므로 `dbsize` 로 세션을 세면 틀린다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 세션만 세려면 접두사로 좁힌다"
|
||||
```bash label="[lab host] ② 세션만 세려면 접두사로 좁힌다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- \
|
||||
redis-cli --scan --pattern '_oauth2_proxy-*'
|
||||
```
|
||||
@@ -347,7 +360,7 @@ _oauth2_proxy=<ticket>|<timestamp>|<mac>
|
||||
|
||||
**무엇을 보는가** — 회전을 치기 전에, 애초에 두 secret 을 동시에 들 수 있는지.
|
||||
|
||||
```bash label="[kc-lab-1] 도움말에서 cookie-secret 을 찾는다"
|
||||
```bash label="[lab host] 도움말에서 cookie-secret 을 찾는다"
|
||||
kubectl -n keycloak-lab exec deploy/oauth2-proxy -- \
|
||||
/bin/oauth2-proxy --help 2>&1 | grep cookie-secret
|
||||
```
|
||||
@@ -373,7 +386,7 @@ kubectl -n keycloak-lab exec deploy/oauth2-proxy -- \
|
||||
|
||||
**행동** — patch 가 지목하는 `env/1` 이 맞는지 먼저 보고, 시각을 남기고, 바꾼다.
|
||||
|
||||
```bash label="[kc-lab-1] ① env 배열의 이름 순서를 본다"
|
||||
```bash label="[lab host] ① env 배열의 이름 순서를 본다"
|
||||
kubectl -n keycloak-lab get deploy oauth2-proxy \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].env[*].name}'; echo
|
||||
```
|
||||
@@ -386,7 +399,7 @@ OAUTH2_PROXY_CLIENT_SECRET OAUTH2_PROXY_COOKIE_SECRET
|
||||
|
||||
`OAUTH2_PROXY_COOKIE_SECRET` 이 몇 번째인지 0부터 센다. 위 모양에서는 두 번째이므로 `env/1` 이고, 순서가 다르면 patch 의 숫자를 고친다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 시각을 UTC 로 남기고 참조를 바꾼다"
|
||||
```bash label="[lab host] ② 시각을 UTC 로 남기고 참조를 바꾼다"
|
||||
date -u '+%H:%M:%S UTC 회전'
|
||||
kubectl -n keycloak-lab patch deployment oauth2-proxy --type=json \
|
||||
-p '[{"op":"replace",
|
||||
@@ -411,7 +424,7 @@ deployment "oauth2-proxy" successfully rolled out
|
||||
|
||||
### 1. Deployment 의 참조가 실제로 바뀌었는가
|
||||
|
||||
```bash label="[kc-lab-1] 지금 참조하는 키 이름을 뽑는다"
|
||||
```bash label="[lab host] 지금 참조하는 키 이름을 뽑는다"
|
||||
kubectl -n keycloak-lab get deploy oauth2-proxy \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.key}'; echo
|
||||
```
|
||||
@@ -426,7 +439,7 @@ kubectl -n keycloak-lab get deploy oauth2-proxy \
|
||||
|
||||
### 2. Redis 는 그대로인가
|
||||
|
||||
```bash label="[kc-lab-1] 주입 전과 똑같은 줄을 친다"
|
||||
```bash label="[lab host] 주입 전과 똑같은 줄을 친다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- \
|
||||
redis-cli --scan --pattern '_oauth2_proxy-*'
|
||||
```
|
||||
@@ -443,7 +456,7 @@ kubectl -n keycloak-lab exec deploy/redis -- \
|
||||
|
||||
### 3. 파드가 실제로 새로 떴는가
|
||||
|
||||
```bash label="[kc-lab-1] 파드 이름이 바뀌었는지 본다"
|
||||
```bash label="[lab host] 파드 이름이 바뀌었는지 본다"
|
||||
kubectl -n keycloak-lab get pods -l app=oauth2-proxy -o wide
|
||||
```
|
||||
|
||||
@@ -461,13 +474,13 @@ Keycloak SSO 세션이 살아 있어서 조용히 재인증이 일어났다. 쿠
|
||||
|
||||
로그가 무슨 일이 났는지 말한다. 먼저 최근 로그를 통째로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 최근 3분을 파드 이름과 함께 본다"
|
||||
```bash label="[lab host] ① 최근 3분을 파드 이름과 함께 본다"
|
||||
kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=3m --prefix
|
||||
```
|
||||
|
||||
`--prefix` 는 각 줄 앞에 파드 이름을 붙여 준다. replica 가 둘이므로 이것이 없으면 누가 무엇을 했는지 못 가린다. `--since=3m` 은 최근 3분만 보므로 브라우저로 접속한 뒤 3분을 넘겨 치면 아무 줄도 안 나온다. 그때 나온 빈 결과는 「로그가 없다」가 아니라 창을 놓쳤다는 뜻이니, 브라우저를 한 번 더 열고 곧바로 친다. 그다음 좁힌다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 세션 저장소 쪽 줄만 좁힌다"
|
||||
```bash label="[lab host] ② 세션 저장소 쪽 줄만 좁힌다"
|
||||
kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=3m | grep -i stored_session
|
||||
```
|
||||
|
||||
@@ -496,7 +509,7 @@ kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=3m | grep -i stored_ses
|
||||
|
||||
그다음 Redis 를 본다.
|
||||
|
||||
```bash label="[kc-lab-1] 세션 키를 다시 센다"
|
||||
```bash label="[lab host] 세션 키를 다시 센다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- \
|
||||
redis-cli --scan --pattern '_oauth2_proxy-*'
|
||||
```
|
||||
@@ -533,7 +546,7 @@ kubectl -n keycloak-lab exec deploy/redis -- \
|
||||
|
||||
덤으로, 로그를 파드별로 갈라 보면 BFF 와 정반대인 성질이 보인다.
|
||||
|
||||
```bash label="[kc-lab-1] 흐름을 시작한 파드와 콜백을 받은 파드를 가른다"
|
||||
```bash label="[lab host] 흐름을 시작한 파드와 콜백을 받은 파드를 가른다"
|
||||
kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=10m --prefix \
|
||||
| grep -E 'Initiating login|AuthSuccess'
|
||||
```
|
||||
@@ -569,7 +582,7 @@ GET "/oauth2/callback?state=..." ← 콜백을 받은 replica
|
||||
|
||||
**행동** — 시각을 남기고 되돌린다.
|
||||
|
||||
```bash label="[kc-lab-1] 되돌리는 것도 회전이다"
|
||||
```bash label="[lab host] 되돌리는 것도 회전이다"
|
||||
date -u '+%H:%M:%S UTC 되돌림'
|
||||
kubectl -n keycloak-lab patch deployment oauth2-proxy --type=json \
|
||||
-p '[{"op":"replace",
|
||||
@@ -596,7 +609,7 @@ kubectl -n keycloak-lab rollout status deploy/oauth2-proxy --timeout=180s
|
||||
|
||||
전부 지울 때는 `b5:pvc` 같은 남의 키를 같이 죽이지 않도록 패턴으로 좁힌다. `FLUSHDB` 를 쓰지 않는다 — 이 Redis 는 BFF 세션도 담고 있다.
|
||||
|
||||
```bash label="[kc-lab-1] 접두사에 걸린 것만 지운다"
|
||||
```bash label="[lab host] 접두사에 걸린 것만 지운다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- \
|
||||
redis-cli --scan --pattern '_oauth2_proxy-*' | while read K; do
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli del "$K"
|
||||
@@ -611,12 +624,12 @@ kubectl -n keycloak-lab exec deploy/redis -- \
|
||||
|
||||
**지우기 전에 백업 파일이 손에 있는지 본다.** 지금은 Grafana Ingress 가 이미 없으니 파일이 비어 있으면 다시 뜰 원본도 없고, 되살리는 절차는 가이드에 없다(unknown). 아래 두 줄의 첫째가 줄 수를 내고 둘째가 `0` 이 아니면 그 파일로 돌려줄 수 있다. `No such file or directory` 가 나오면 ①을 치지 않는다 — B-4 로 app2 를 빌린 적이 있으면 그 편은 같은 백업을 `/tmp/grafana-ingress-backup.yaml` 에 떠 두므로 그쪽을 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ⓪ 백업 파일이 쓸 만한지 본다"
|
||||
```bash label="[lab host] ⓪ 백업 파일이 쓸 만한지 본다"
|
||||
wc -l ~/grafana-ingress-backup.yaml
|
||||
grep -c 'app2.hyeonworks.com' ~/grafana-ingress-backup.yaml
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ① 빌린 것을 걷고 백업을 올린다"
|
||||
```bash label="[lab host] ① 빌린 것을 걷고 백업을 올린다"
|
||||
kubectl -n keycloak-lab delete ingress oauth2-proxy
|
||||
kubectl apply -f ~/grafana-ingress-backup.yaml
|
||||
```
|
||||
@@ -663,6 +676,14 @@ curl -sI https://app2.hyeonworks.com/ | head -3
|
||||
| `app2` 가 Grafana 로 간다 | Ingress 를 안 만들었거나 이미 복구했다 | `kubectl -n keycloak-lab get ingress` |
|
||||
| 실험이 끝났는데 Grafana 가 안 열린다 | Ingress 복구를 안 했다 | oauth2-proxy Ingress 를 먼저 지우고 백업을 올린다 |
|
||||
|
||||
## 이 실험대에서 아직 못 밟은 단계
|
||||
|
||||
2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.**
|
||||
|
||||
- **남은 것** — 쿠키 비밀 회전 전부. `oauth2-proxy` 가 기동 시 OIDC 디스커버리를 `https` 로 하므로 인증서가 서야 뜬다.
|
||||
- **막는 것** — `https://auth.hyeonworks.com` 이 서지 않는다. 와일드카드 인증서(Cloudflare API 토큰이 필요한 DNS-01)와, 밖에서 실험대에 닿는 길(호스트의 libvirt `guest_input` 구멍 — A-4 에서 확인한 `ExecStartPost` 누락)이 둘 다 있어야 한다.
|
||||
- **그때까지 이 편의 실측 가운데 `(observed)` 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다.** 나머지는 원래 실행의 값이다.
|
||||
|
||||
## 무엇이 관측이고 무엇이 아닌가
|
||||
|
||||
이 절차의 숫자는 `2026-09-04 14:35–14:42 KST` 에 돈 한 번의 실행에서 나왔다(observed). 증거의 로그가 `[2026/09/04 05:41:46]` 인 것과 수집 시각이 `14:35–14:42 KST` 인 것은 같은 순간이다(KST = UTC+9). 이 어긋남을 모르고 로그를 뒤지면 9시간 전을 뒤지게 된다.
|
||||
|
||||
+58
-27
@@ -37,7 +37,9 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 전부 `[kc-lab-1]` 에서 `kubectl` 로 친다. Redis 에 묻는 것도 `kubectl exec deploy/redis` 를 거치므로 노드에 들어갈 일이 없다.
|
||||
명령은 전부 `[lab host]` 에서 `kubectl` 로 친다. Redis 에 묻는 것도 `kubectl exec deploy/redis` 를 거치므로 노드에 들어갈 일이 없다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
브라우저도 필요하다. 고아는 사람이 옛 쿠키를 들고 와야 생긴다. 셸만으로는 아무리 회전시켜도 키가 늘지 않는다.
|
||||
|
||||
@@ -85,11 +87,22 @@ B-7 은 여기서 멈췄다.
|
||||
- 브라우저가 있어야 한다.
|
||||
- 기동 로그의 `refresh:disabled` 를 1 절에서 확인한다. **그것이 `disabled` 가 아니면 이 절차의 정리 규칙은 그 환경에서 성립하지 않는다.**
|
||||
|
||||
**`oauth2-proxy` 는 인증서가 서기 전에는 못 뜬다.** 기동할 때 OIDC 디스커버리를 먼저 하는데 그 주소가 `https://auth.hyeonworks.com/...` 이라, 인증서 단계를 안 끝낸 실험대에서는 `CrashLoopBackOff` 로 끝난다. 2026-09-17 에 그대로 겪었다(observed).
|
||||
|
||||
```text
|
||||
[provider.go:55] Performing OIDC Discovery...
|
||||
[main.go:59] ERROR: Failed to initialise OAuth2 Proxy: ... error while discovery OIDC configuration:
|
||||
Get "https://auth.hyeonworks.com/realms/keycloak-patterns/.well-known/openid-configuration":
|
||||
dial tcp 100.83.212.4:443: connect: connection refused
|
||||
```
|
||||
|
||||
`oauth2-proxy-secrets` 가 있고 이미지도 받아지는데 기동 자체가 안 된다 — **이 편의 전제는 배포가 아니라 TLS 다.**
|
||||
|
||||
**이건 남의 세션을 실제로 지우는 실험이다.** `redis-cli del` 로 세션 키를 지우고, 산 사람의 세션을 잘못 지우면 그 사람은 재로그인해야 한다. SSO 가 살아 있으면 조용히 지나간다. 그 이상의 피해는 측정되지 않았지만 실험대에서만 한다.
|
||||
|
||||
되돌리기는 secret 참조를 A 로 되돌리는 한 줄이다. B-7 에서 Grafana 의 Ingress 를 빌렸다면 이 절차가 끝난 뒤에 돌려준다.
|
||||
|
||||
```bash label="[kc-lab-1] 중간에 그만둘 때 치는 한 줄"
|
||||
```bash label="[lab host] 중간에 그만둘 때 치는 한 줄"
|
||||
kubectl -n keycloak-lab patch deployment oauth2-proxy --type=json \
|
||||
-p '[{"op":"replace",
|
||||
"path":"/spec/template/spec/containers/0/env/1/valueFrom/secretKeyRef/key",
|
||||
@@ -106,7 +119,7 @@ kubectl -n keycloak-lab patch deployment oauth2-proxy --type=json \
|
||||
|
||||
**무엇을 보는가** — 기동 로그의 쿠키 설정 한 줄. 이 한 단어가 정리 규칙 전체의 전제다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 기동 로그에서 쿠키 설정을 찾는다"
|
||||
```bash label="[lab host] ① 기동 로그에서 쿠키 설정을 찾는다"
|
||||
kubectl -n keycloak-lab logs -l app=oauth2-proxy | grep 'Cookie settings'
|
||||
```
|
||||
|
||||
@@ -123,7 +136,7 @@ kubectl -n keycloak-lab logs -l app=oauth2-proxy | grep 'Cookie settings'
|
||||
|
||||
기동 로그가 잘려 나갔으면 인자에서 직접 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 인자에서 쿠키 관련 값만 뽑는다"
|
||||
```bash label="[lab host] ② 인자에서 쿠키 관련 값만 뽑는다"
|
||||
kubectl -n keycloak-lab get deploy oauth2-proxy \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].args}' | tr ',' '\n' | grep -i cookie
|
||||
```
|
||||
@@ -143,12 +156,22 @@ kubectl -n keycloak-lab get deploy oauth2-proxy \
|
||||
|
||||
브라우저에서 `https://app2.hyeonworks.com/api/echo` 를 열고 `labuser` / `labpass` 로 로그인한다. 업스트림의 JSON 이 보이면 세션이 생겼다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 세션 키와 전체 키 수를 본다"
|
||||
```bash label="[lab host] ① 세션 키와 전체 키 수를 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- \
|
||||
redis-cli --scan --pattern '_oauth2_proxy-*'
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||||
```
|
||||
|
||||
**두 명령 자체는 2026-09-17 에 쳐서 돌았다**(observed). 세션이 없는 상태에서는 이렇게 나온다.
|
||||
|
||||
```text
|
||||
(--scan --pattern '_oauth2_proxy-*' 는 아무것도 안 낸다)
|
||||
0
|
||||
```
|
||||
|
||||
`dbsize` 가 `0` 이고 스캔이 비었다는 것은 **Redis 는 멀쩡한데 엣지 세션이 하나도 없다**는 뜻이다. `oauth2-proxy` 가 안 떠 있으면 늘 이 모양이므로, 이 값을 보고 「Redis 가 안 붙었다」로 읽지 않는다.
|
||||
|
||||
|
||||
**어디를 보나** — 실측은 이렇다(observed, `01-orphan-lifecycle.txt`).
|
||||
|
||||
```text
|
||||
@@ -161,7 +184,7 @@ kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||||
|
||||
그 키 하나에 대해 셋을 따로 묻는다. 나중에 루프로 묶더라도 처음에는 `type` · `ttl` · `strlen` 이 각각 무엇을 답하는지 봐 둔다. 키 이름은 위 출력에서 가져온다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 한 키에 세 가지를 따로 묻는다"
|
||||
```bash label="[lab host] ② 한 키에 세 가지를 따로 묻는다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- \
|
||||
redis-cli type _oauth2_proxy-f6a9201fd534a047998278452001ccbf
|
||||
kubectl -n keycloak-lab exec deploy/redis -- \
|
||||
@@ -184,7 +207,7 @@ kubectl -n keycloak-lab exec deploy/redis -- \
|
||||
|
||||
**이 절은 건너뛸 수 없다.** 7 절부터 끝까지 모든 Redis 조회가 이 `R` 을 부른다. 그리고 셸 함수는 그 셸에만 있다 — 터미널이 끊기거나 다른 창에서 이어 치면 `R: command not found` 가 나오고, 그때는 이 블록의 첫 줄부터 다시 친다.
|
||||
|
||||
```bash label="[kc-lab-1] 이름 그대로 하는 한 줄짜리 함수 (unknown)"
|
||||
```bash label="[lab host] 이름 그대로 하는 한 줄짜리 함수 (unknown)"
|
||||
R() { kubectl -n keycloak-lab exec deploy/redis -- redis-cli "$@"; }
|
||||
R --scan --pattern '_oauth2_proxy-*' | while read K; do
|
||||
echo "$K type=$(R type $K) ttl=$(R ttl $K) len=$(R strlen $K)"
|
||||
@@ -203,7 +226,7 @@ _oauth2_proxy-f6a9201fd534a047998278452001ccbf type=string ttl=3568 len=3510
|
||||
|
||||
**무엇을 보는가** — 셸의 시각과 UTC, 그리고 NTP 동기화 여부.
|
||||
|
||||
```bash label="[kc-lab-1] 로컬과 UTC 를 나란히 보고 동기화를 확인한다"
|
||||
```bash label="[lab host] 로컬과 UTC 를 나란히 보고 동기화를 확인한다"
|
||||
date; date -u
|
||||
timedatectl show -p NTP -p NTPSynchronized
|
||||
```
|
||||
@@ -225,11 +248,11 @@ NTPSynchronized=yes
|
||||
|
||||
**행동** — 시각을 먼저 담고 참조를 바꾼다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 회전 시각을 UTC 로 담는다"
|
||||
```bash label="[lab host] ① 회전 시각을 UTC 로 담는다"
|
||||
ROT=$(date -u +%s); echo "회전 $ROT ($(date -u -d @$ROT +%H:%M:%S) UTC)"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② secret 참조를 B 로 바꾼다"
|
||||
```bash label="[lab host] ② secret 참조를 B 로 바꾼다"
|
||||
kubectl -n keycloak-lab patch deployment oauth2-proxy --type=json \
|
||||
-p '[{"op":"replace",
|
||||
"path":"/spec/template/spec/containers/0/env/1/valueFrom/secretKeyRef/key",
|
||||
@@ -251,7 +274,7 @@ kubectl -n keycloak-lab rollout status deploy/oauth2-proxy --timeout=180s
|
||||
|
||||
### 6. Deployment 의 참조가 바뀌었는가
|
||||
|
||||
```bash label="[kc-lab-1] 지금 참조하는 키 이름을 뽑는다"
|
||||
```bash label="[lab host] 지금 참조하는 키 이름을 뽑는다"
|
||||
kubectl -n keycloak-lab get deploy oauth2-proxy \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.key}'; echo
|
||||
```
|
||||
@@ -266,7 +289,7 @@ COOKIE_SECRET_B
|
||||
|
||||
주입 전과 똑같은 명령으로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] 키와 TTL 을 다시 본다"
|
||||
```bash label="[lab host] 키와 TTL 을 다시 본다"
|
||||
R --scan --pattern '_oauth2_proxy-*' | while read K; do
|
||||
echo "$K ttl=$(R ttl $K)"
|
||||
done
|
||||
@@ -288,7 +311,7 @@ R dbsize
|
||||
|
||||
로그인했던 그 브라우저 그대로 `https://app2.hyeonworks.com/api/echo` 를 연다.
|
||||
|
||||
```bash label="[kc-lab-1] 최근 2분의 세션 저장소 로그를 좁힌다"
|
||||
```bash label="[lab host] 최근 2분의 세션 저장소 로그를 좁힌다"
|
||||
kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=2m | grep stored_session
|
||||
```
|
||||
|
||||
@@ -306,7 +329,7 @@ kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=2m | grep stored_sessio
|
||||
|
||||
**이 값이 뜻하는 것** — `AuthSuccess` 의 시각 `11:30:27` 을 적어 둔다. 13 절에서 이 숫자와 역산값을 맞춰 본다. 그리고 Redis 를 다시 본다.
|
||||
|
||||
```bash label="[kc-lab-1] 키가 늘었는지 본다"
|
||||
```bash label="[lab host] 키가 늘었는지 본다"
|
||||
R --scan --pattern '_oauth2_proxy-*' | while read K; do
|
||||
echo "$K ttl=$(R ttl $K)"
|
||||
done
|
||||
@@ -330,7 +353,7 @@ R dbsize
|
||||
|
||||
**무엇을 보는가** — 두 키의 네 가지 성질.
|
||||
|
||||
```bash label="[kc-lab-1] 두 키를 나란히 놓는다"
|
||||
```bash label="[lab host] 두 키를 나란히 놓는다"
|
||||
R --scan --pattern '_oauth2_proxy-*' | while read K; do
|
||||
echo "$K type=$(R type $K) len=$(R strlen $K) ttl=$(R ttl $K)"
|
||||
done
|
||||
@@ -357,7 +380,7 @@ done
|
||||
|
||||
아래 키 이름은 이 실험대에서 나온 값이라 그대로 치면 남의 키를 조회해 `(nil)` 이 돌아온다. 2 절의 `--scan` 출력에서 자기 키 이름을 옮겨 넣는다.
|
||||
|
||||
```bash label="[kc-lab-1] 값의 앞머리만 이스케이프해서 본다 — 키 이름은 자기 것으로"
|
||||
```bash label="[lab host] 값의 앞머리만 이스케이프해서 본다 — 키 이름은 자기 것으로"
|
||||
R --no-raw get _oauth2_proxy-f6a9201fd534a047998278452001ccbf | head -c 120; echo
|
||||
```
|
||||
|
||||
@@ -375,7 +398,7 @@ R --no-raw get _oauth2_proxy-f6a9201fd534a047998278452001ccbf | head -c 120; ech
|
||||
|
||||
**무엇을 보는가** — 둘이다. TTL 이 실제로 줄어드는가, 요청을 보내면 되살아나는가. 30초 간격으로 세 번이고 여기에 1분이 그대로 든다.
|
||||
|
||||
```bash label="[kc-lab-1] 30초 간격으로 세 번 재는 루프 (unknown)"
|
||||
```bash label="[lab host] 30초 간격으로 세 번 재는 루프 (unknown)"
|
||||
for i in 1 2 3; do
|
||||
date -u '+%H:%M:%S'
|
||||
R --scan --pattern '_oauth2_proxy-*' | while read K; do
|
||||
@@ -421,7 +444,7 @@ done
|
||||
|
||||
**행동** — 되돌리기가 없는 조작이니 지우기 전에 어느 키인지 두 번 확인한다. 지금은 TTL 이 작은 쪽이 고아다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 고아 하나를 지우고 남은 것을 센다"
|
||||
```bash label="[lab host] ① 고아 하나를 지우고 남은 것을 센다"
|
||||
R del _oauth2_proxy-f6a9201fd534a047998278452001ccbf
|
||||
R dbsize
|
||||
R --scan --pattern '_oauth2_proxy-*'
|
||||
@@ -438,7 +461,7 @@ R --scan --pattern '_oauth2_proxy-*'
|
||||
|
||||
반환값이 `1` 이다. `0` 이면 그 키가 없었던 것이고 이름을 잘못 옮겼다. 산 세션이 멀쩡한지는 브라우저로 다시 열어서 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 삭제 직후 요청이 200 인지 로그로 본다"
|
||||
```bash label="[lab host] ② 삭제 직후 요청이 200 인지 로그로 본다"
|
||||
kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=1m | grep labuser
|
||||
```
|
||||
|
||||
@@ -461,7 +484,7 @@ kubectl -n keycloak-lab logs -l app=oauth2-proxy --since=1m | grep labuser
|
||||
|
||||
**행동** — 2차 회전 시각을 담고 B → A 로 되돌린다.
|
||||
|
||||
```bash label="[kc-lab-1] 2차 회전 시각을 담고 참조를 A 로 바꾼다"
|
||||
```bash label="[lab host] 2차 회전 시각을 담고 참조를 A 로 바꾼다"
|
||||
ROT2=$(date -u +%s); echo "2차 회전 $ROT2 ($(date -u -d @$ROT2 +%H:%M:%S) UTC)"
|
||||
kubectl -n keycloak-lab patch deployment oauth2-proxy --type=json \
|
||||
-p '[{"op":"replace",
|
||||
@@ -472,7 +495,7 @@ kubectl -n keycloak-lab rollout status deploy/oauth2-proxy --timeout=180s
|
||||
|
||||
그리고 브라우저로 다시 연 뒤 본다.
|
||||
|
||||
```bash label="[kc-lab-1] 키와 TTL 을 다시 본다"
|
||||
```bash label="[lab host] 키와 TTL 을 다시 본다"
|
||||
R --scan --pattern '_oauth2_proxy-*' | while read K; do
|
||||
echo "$K ttl=$(R ttl $K)"
|
||||
done
|
||||
@@ -523,7 +546,7 @@ done
|
||||
|
||||
**기준 시각을 먼저 정한다.** 이 루프가 재는 것은 「지금 걷어내려는 회전보다 앞에 만들어졌나」다. 5 절에서 담은 `ROT` 은 1차 회전 시각이고, 12 절을 쳤으면 지금 유효한 회전은 2차이므로 기준은 `ROT2` 다. **12 절을 친 뒤라면 아래 두 블록의 `"$ROT"` 를 `"$ROT2"` 로 바꿔 친다.** 안 바꾸면 두 회전 사이에 생긴 세션이 「산것」으로 분류되어 그대로 남고, 15 절의 실측(`87faa1c9…` 삭제 · 남은 `dbsize=1`)이 재현되지 않는다 — 그 키의 역산 생성시각 `11:30:26` 은 `ROT`(`11:29:56`)보다 뒤이기 때문이다.
|
||||
|
||||
```bash label="[kc-lab-1] 지우지 않고 판정만 하는 루프 — 12 절을 쳤으면 $ROT 을 $ROT2 로 (unknown)"
|
||||
```bash label="[lab host] 지우지 않고 판정만 하는 루프 — 12 절을 쳤으면 $ROT 을 $ROT2 로 (unknown)"
|
||||
NOW=$(date -u +%s); EXP=3600
|
||||
R --scan --pattern '_oauth2_proxy-*' | while read K; do
|
||||
T=$(R ttl "$K"); C=$(( NOW - (EXP - T) ))
|
||||
@@ -549,7 +572,7 @@ done
|
||||
|
||||
**리허설이 보장하는 범위.** 두 블록은 `NOW` 를 각각 새로 잡고 `C = NOW − (EXP − T)` 로 역산한다. `NOW` 와 `T` 가 같이 흐르므로 `C` 는 대체로 같은 값이 나오지만, 11 절이 적었듯 TTL 이 초 단위 정수라 ±1초가 반올림으로 흔들린다. **생성시각이 기준 회전 시각의 ±1초 안에 놓인 키는 리허설에서 「산것」이었다가 실행에서 「고아」로 뒤집힐 수 있고, 그쪽 방향의 오판이 곧 산 세션 삭제다.** 리허설 출력의 생성시각이 기준 시각에 붙어 있는 키가 보이면 그 키는 이 루프로 지우지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] 고아로 판정된 것만 지운다 — 14 절과 같은 기준으로 (unknown)"
|
||||
```bash label="[lab host] 고아로 판정된 것만 지운다 — 14 절과 같은 기준으로 (unknown)"
|
||||
NOW=$(date -u +%s); EXP=3600
|
||||
R --scan --pattern '_oauth2_proxy-*' | while read K; do
|
||||
T=$(R ttl "$K"); C=$(( NOW - (EXP - T) ))
|
||||
@@ -571,7 +594,7 @@ R dbsize
|
||||
|
||||
**문제가 생기면** — 전제가 깨졌을 때, 즉 `--cookie-refresh` 가 켜져 있을 때는 위 규칙을 쓰지 않는다. 전부 지우고 모두 재인증시킨다. `FLUSHDB` 를 쓰지 않는다 — 이 Redis 에는 BFF 세션도 들어 있어 패턴으로 좁히는 것이 이 실험대에서는 필수다.
|
||||
|
||||
```bash label="[kc-lab-1] 전제가 깨졌을 때 — 접두사에 걸린 것만 전부 지운다"
|
||||
```bash label="[lab host] 전제가 깨졌을 때 — 접두사에 걸린 것만 전부 지운다"
|
||||
R --scan --pattern '_oauth2_proxy-*' | while read K; do R del "$K"; done
|
||||
```
|
||||
|
||||
@@ -581,7 +604,7 @@ R --scan --pattern '_oauth2_proxy-*' | while read K; do R del "$K"; done
|
||||
|
||||
**행동** — 참조를 확인하고, Grafana Ingress 를 돌려준다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 지금 참조가 A 인지 본다"
|
||||
```bash label="[lab host] ① 지금 참조가 A 인지 본다"
|
||||
kubectl -n keycloak-lab get deploy oauth2-proxy \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.key}'; echo
|
||||
```
|
||||
@@ -590,14 +613,14 @@ kubectl -n keycloak-lab get deploy oauth2-proxy \
|
||||
|
||||
**돌려주기 전에 백업 파일이 손에 있는지 본다.** 이 파일은 이 편이 만들지 않는다 — B-7 이 Grafana Ingress 를 걷어내기 **전에** 떠 둔다. 지금은 그 Ingress 가 이미 없으니 파일이 없으면 다시 뜰 수도 없고, 되살리는 경로는 가이드에 없다(unknown). 그러니 없으면 아래 `delete` 를 치지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 백업 파일이 쓸 만한지 본다"
|
||||
```bash label="[lab host] ② 백업 파일이 쓸 만한지 본다"
|
||||
wc -l ~/grafana-ingress-backup.yaml
|
||||
grep -c 'app2.hyeonworks.com' ~/grafana-ingress-backup.yaml
|
||||
```
|
||||
|
||||
줄 수가 나오고 둘째 줄이 `0` 이 아니면 그 파일로 돌려줄 수 있다. `No such file or directory` 면 여기서 멈춘다. B-4 로 app2 를 빌린 적이 있다면 그 편은 같은 백업을 `/tmp/grafana-ingress-backup.yaml` 에 떠 두므로 그쪽도 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 빌린 Ingress 를 걷고 백업을 올린 뒤 밖에서 본다"
|
||||
```bash label="[lab host] ③ 빌린 Ingress 를 걷고 백업을 올린 뒤 밖에서 본다"
|
||||
kubectl -n keycloak-lab delete ingress oauth2-proxy
|
||||
kubectl apply -f ~/grafana-ingress-backup.yaml
|
||||
curl -sI https://app2.hyeonworks.com/ | head -3
|
||||
@@ -640,6 +663,14 @@ curl -sI https://app2.hyeonworks.com/ | head -3
|
||||
| 루프가 너무 느리다 | 키마다 `kubectl exec` 를 한다 | `--scan` 결과를 먼저 받아 두고 필요한 것만 묻는다 |
|
||||
| 로그 시각이 9시간 어긋난다 | 프록시 로그는 UTC | 표시 규약 |
|
||||
|
||||
## 이 실험대에서 아직 못 밟은 단계
|
||||
|
||||
2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.**
|
||||
|
||||
- **남은 것** — 고아 세션 관찰 전부. 같은 이유로 `oauth2-proxy` 가 안 뜬다. 지금까지 밟은 것 — Redis 쪽 명령(`dbsize`·`--scan`)이 도는 것까지.
|
||||
- **막는 것** — `https://auth.hyeonworks.com` 이 서지 않는다. 와일드카드 인증서(Cloudflare API 토큰이 필요한 DNS-01)와, 밖에서 실험대에 닿는 길(호스트의 libvirt `guest_input` 구멍 — A-4 에서 확인한 `ExecStartPost` 누락)이 둘 다 있어야 한다.
|
||||
- **그때까지 이 편의 실측 가운데 `(observed)` 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다.** 나머지는 원래 실행의 값이다.
|
||||
|
||||
## 무엇이 관측이고 무엇이 아닌가
|
||||
|
||||
이 절차의 숫자는 `2026-09-04 11:29–11:34 UTC` 에 돈 한 번의 실행에서 나왔다(observed). 시각은 전부 UTC 로 다룬다 — 이 실험의 결론이 시각 계산이라 KST 와 섞이면 9시간이 틀어진다.
|
||||
|
||||
+54
-30
@@ -44,7 +44,9 @@ app1 과 app2 를 한 번의 로그인으로 묶은 뒤 IdP 세션만 끊고,
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 `kc-lab-1` 에서 `kubectl` 로 친다. `kubectl` 에 `sudo` 를 붙이지 않는다. 반입한 가이드의 전제 문장은 `sudo kubectl` 로 적혀 있지만 같은 폴더의 README 가 반대로 적고, 본문 명령 블록에도 `sudo kubectl` 은 한 번도 없다. `sudo` 를 붙이면 root 환경으로 돌아 사용자 홈의 kubeconfig 를 못 본다.
|
||||
명령은 `[lab host]` 에서 `kubectl` 로 친다. `kubectl` 에 `sudo` 를 붙이지 않는다. 반입한 가이드의 전제 문장은 `sudo kubectl` 로 적혀 있지만 같은 폴더의 README 가 반대로 적고, 본문 명령 블록에도 `sudo kubectl` 은 한 번도 없다. `sudo` 를 붙이면 root 환경으로 돌아 사용자 홈의 kubeconfig 를 못 본다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
`[밖에서]` 라벨이 붙은 `curl` 은 클러스터 밖에서 공개 이름을 두드린다는 뜻이다. 어느 기계에서 치라는 줄은 가이드에 없으므로(unknown), `https://app1.hyeonworks.com` 이 풀리는 기계면 어디서든 친다. 거기로 가는 `ssh` 명령도 가이드에 없다.
|
||||
|
||||
@@ -116,7 +118,7 @@ app1 과 app2 를 한 번의 로그인으로 묶은 뒤 IdP 세션만 끊고,
|
||||
|
||||
되돌리기는 둘이고 둘 다 먼저 읽어 둔다. 하나는 세션을 다시 깨끗하게 만드는 것이라 주입 전 절차와 같은 명령이다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 세션을 비우고 캐시를 버린다"
|
||||
```bash label="[lab host] ① 세션을 비우고 캐시를 버린다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "delete from offline_client_session" -c "delete from offline_user_session"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli flushall
|
||||
@@ -126,7 +128,7 @@ kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s
|
||||
|
||||
다른 하나는 빌린 이름을 돌려준다. ②는 먼저 지우고 나중에 올리므로, 백업 파일이 비어 있으면 Grafana 가 안 열리는 채로 끝난다. 실제로 칠 때는 복구 3 절의 확인 두 줄을 먼저 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 빌린 Ingress 를 걷고 Grafana 것을 올린다"
|
||||
```bash label="[lab host] ② 빌린 Ingress 를 걷고 Grafana 것을 올린다"
|
||||
kubectl -n keycloak-lab delete ingress oauth2-proxy
|
||||
kubectl apply -f ~/grafana-ingress-backup.yaml
|
||||
```
|
||||
@@ -153,7 +155,7 @@ C-2 를 이어서 할 생각이면 아직 돌려주지 않는다. C-2 가 두
|
||||
|
||||
**행동** — 클러스터 안을 먼저 보고, 밖에서 두 이름을 두드린다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 파드와 Ingress 를 통째로 본다"
|
||||
```bash label="[lab host] ① 파드와 Ingress 를 통째로 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
kubectl -n keycloak-lab get ingress
|
||||
```
|
||||
@@ -179,7 +181,7 @@ curl -s -o /dev/null -w 'app2 %{http_code}\n' https://app2.hyeonworks.com/
|
||||
|
||||
**행동** — 관리자 비밀번호를 Secret 에서 읽어 명령 치환으로 넘긴다.
|
||||
|
||||
```bash label="[kc-lab-1] 비밀번호를 화면에 찍지 않고 로그인한다"
|
||||
```bash label="[lab host] 비밀번호를 화면에 찍지 않고 로그인한다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
config credentials --server http://localhost:8080 --realm master --user admin \
|
||||
--password "$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
@@ -198,14 +200,14 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
**행동** — 관리 API 를 치고 남은 세션을 센다.
|
||||
|
||||
```bash label="[kc-lab-1] ① realm 전체 로그아웃을 건다"
|
||||
```bash label="[lab host] ① realm 전체 로그아웃을 건다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
create realms/keycloak-patterns/logout-all
|
||||
```
|
||||
|
||||
이 실험대는 세는 쪽을 스크립트로 돌렸고 증거에 SQL 원문이 없다. 따라 하는 사람은 가이드가 손으로 치기 좋게 고친 아래 형태를 친다. 가이드가 미검증으로 표시한 줄이다(unknown).
|
||||
이 실험대는 세는 쪽을 스크립트로 돌렸고 증거에 SQL 원문이 없다. 따라 하는 사람은 가이드가 손으로 치기 좋게 고친 아래 형태를 친다. **2026-09-17 에 쳐서 돌았다**(observed) — 그때까지 미검증이던 줄이다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 남은 세션을 센다"
|
||||
```bash label="[lab host] ② 남은 세션을 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from offline_user_session where offline_flag='0'"
|
||||
```
|
||||
@@ -231,32 +233,46 @@ DELETE 1
|
||||
|
||||
**목적** — `keycloak-patterns` 세션 수와 Redis 키를 둘 다 0 으로 만든다.
|
||||
|
||||
**2026-09-17 실측**(observed) — `logout-all` 을 친 **뒤에도** 세션이 그대로 `6` 건이었다. 이 방법으로는 안 지워진다는 것이 이 절의 결론이고, 그래서 아래 4절로 간다.
|
||||
|
||||
**행동** — 자식 테이블부터 지우고, 앱 세션을 비우고, 프로세스를 새로 띄운다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 자식 테이블부터 지운다"
|
||||
```bash label="[lab host] ① 자식 테이블부터 지운다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "delete from offline_client_session" -c "delete from offline_user_session"
|
||||
```
|
||||
|
||||
모양은 이렇다(observed).
|
||||
2026-09-17 실측이다(observed). **자식 쪽이 훨씬 많다** — 부모부터 지우려 했다면 외래키에 걸렸을 것이다.
|
||||
|
||||
```text
|
||||
DELETE 1633
|
||||
DELETE 6
|
||||
```
|
||||
|
||||
원래 실행의 모양은 이렇다(observed).
|
||||
|
||||
```text
|
||||
DELETE 2
|
||||
DELETE 4
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 앱 세션도 비운다"
|
||||
```bash label="[lab host] ② 앱 세션도 비운다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli flushall
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ 캐시를 버리려면 프로세스를 새로 띄운다"
|
||||
```bash label="[lab host] ③ 캐시를 버리려면 프로세스를 새로 띄운다"
|
||||
kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s
|
||||
```
|
||||
|
||||
③ 이 끝나면 `keycloak-0` 파드가 새로 뜨고 그 안에 있던 kcadm 세션 파일이 함께 사라진다. 그러니 롤아웃이 끝나는 대로 주입 전 2 절의 `config credentials` 를 다시 친다. 건너뛰면 뒤에 나오는 `kcadm` 이 전부 `401` 을 내는데, 그 사실은 관찰 1 절에 가서야 보인다.
|
||||
③ 이 끝나면 `keycloak-0` 파드가 새로 뜨고 그 안에 있던 kcadm 세션 파일이 함께 사라진다. 2026-09-17 에 그대로 겪었다(observed) — 롤아웃 직후 `kcadm` 이 이렇게 끝난다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 두 저장소를 다시 센다"
|
||||
```text
|
||||
No server specified. Use --server, or 'kcadm.sh config credentials'.
|
||||
```
|
||||
그러니 롤아웃이 끝나는 대로 주입 전 2 절의 `config credentials` 를 다시 친다. 건너뛰면 뒤에 나오는 `kcadm` 이 전부 `401` 을 내는데, 그 사실은 관찰 1 절에 가서야 보인다.
|
||||
|
||||
```bash label="[lab host] ④ 두 저장소를 다시 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from offline_user_session where offline_flag='0'"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '*'
|
||||
@@ -286,14 +302,14 @@ Redis 키 0, 세션 수 0 이어야 한다.
|
||||
|
||||
**행동** — 두 형태를 나란히 친다. 이 실험대가 쓴 쪽이 틀린 방법이다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 전체를 세는 쪽 — 틀린 방법이다"
|
||||
```bash label="[lab host] ① 전체를 세는 쪽 — 틀린 방법이다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from offline_user_session where offline_flag='0'"
|
||||
```
|
||||
|
||||
따라 하는 사람은 `realm` 을 조인한다. 가이드가 미검증으로 표시한 형태다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② realm 을 조인하는 쪽"
|
||||
```bash label="[lab host] ② realm 을 조인하는 쪽"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"select us.user_session_id, r.name as realm,
|
||||
(select count(*) from offline_client_session cs
|
||||
@@ -336,7 +352,7 @@ https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/aut
|
||||
|
||||
**행동** — 주입 전 5 절에서 고친 조인 쿼리에 realm 조건을 붙여 친다. 같은 이유로 미검증이다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ① keycloak-patterns 세션만 본다"
|
||||
```bash label="[lab host] ① keycloak-patterns 세션만 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"select us.user_session_id, r.name as realm,
|
||||
(select count(*) from offline_client_session cs
|
||||
@@ -355,7 +371,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c
|
||||
(1 row)
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 앱 세션 쪽을 본다"
|
||||
```bash label="[lab host] ② 앱 세션 쪽을 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '*'
|
||||
```
|
||||
|
||||
@@ -369,7 +385,7 @@ kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '*'
|
||||
|
||||
`authorized client` 를 세는 줄도 미검증이다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ③ 토큰이 들어간 행을 센다"
|
||||
```bash label="[lab host] ③ 토큰이 들어간 행을 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from oauth2_authorized_client"
|
||||
```
|
||||
@@ -398,7 +414,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
### 1. 같은 user session 인가
|
||||
|
||||
```bash label="[kc-lab-1] 주입 전과 똑같은 줄을 친다 — user session 과 client session 수"
|
||||
```bash label="[lab host] 주입 전과 똑같은 줄을 친다 — user session 과 client session 수"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"select us.user_session_id, r.name as realm,
|
||||
(select count(*) from offline_client_session cs
|
||||
@@ -431,7 +447,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c
|
||||
|
||||
아래 쿼리의 `oqOjHekin4JU-BZjgQLjUByW` 는 원래 실행의 세션 id 다. 치기 전에 주입 2 절에서 적어 둔 자기 값으로 갈아 끼운다. 그대로 치면 조건에 걸리는 행이 없어 `(0 rows)` 가 나오고, 그것을 「두 앱이 안 붙었다」로 읽으면 판정이 뒤집힌다.
|
||||
|
||||
```bash label="[kc-lab-1] 클라이언트 이름을 조인해서 본다"
|
||||
```bash label="[lab host] 클라이언트 이름을 조인해서 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"select cs.client_id, c.client_id as name
|
||||
from offline_client_session cs join client c on c.id = cs.client_id
|
||||
@@ -455,7 +471,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c
|
||||
|
||||
### 3. 저장소에는 무엇이 늘었는가
|
||||
|
||||
```bash label="[kc-lab-1] 주입 전과 똑같은 줄을 친다 — Redis 키 목록"
|
||||
```bash label="[lab host] 주입 전과 똑같은 줄을 친다 — Redis 키 목록"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '*'
|
||||
```
|
||||
|
||||
@@ -490,7 +506,7 @@ Keycloak 은 세션을 `user session`(사람 하나)과 `client session`(그 사
|
||||
|
||||
지우는 방법을 고르는 데서 하나가 걸러진다. 가이드가 미검증으로 표시했다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] 세션 id 로 지운다 — 안 먹는다"
|
||||
```bash label="[lab host] 세션 id 로 지운다 — 안 먹는다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
delete sessions/oqOjHekin4JU-BZjgQLjUByW -r keycloak-patterns
|
||||
```
|
||||
@@ -505,7 +521,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
**행동** — 사용자 id 를 먼저 잡아 눈으로 확인하고, 그다음에 로그아웃을 건다.
|
||||
|
||||
```bash label="[kc-lab-1] 두 단계로 나눠 친다"
|
||||
```bash label="[lab host] 두 단계로 나눠 친다"
|
||||
USERID=$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get users -r keycloak-patterns -q username=labuser --fields id \
|
||||
--format csv --noquotes | tail -1)
|
||||
@@ -523,7 +539,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
### 3. IdP 세션이 realm 별로 어떻게 남았는지 본다
|
||||
|
||||
```bash label="[kc-lab-1] realm 을 조인해서 센다"
|
||||
```bash label="[lab host] realm 을 조인해서 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"select us.user_session_id, r.name as realm,
|
||||
(select count(*) from offline_client_session cs
|
||||
@@ -555,7 +571,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c
|
||||
|
||||
### 4. 앱 세션을 주입 검증과 같은 명령으로 본다
|
||||
|
||||
```bash label="[kc-lab-1] 앞에서 친 것과 똑같은 줄이다"
|
||||
```bash label="[lab host] 앞에서 친 것과 똑같은 줄이다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '*'
|
||||
```
|
||||
|
||||
@@ -597,7 +613,7 @@ kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '*'
|
||||
|
||||
실제로 끊기는 순간을 보려면 수명 두 값을 읽고 기다린다. 가이드는 이 줄을 미검증으로 표시했고 기다려서 확인하지도 않았다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] 수명 두 값을 읽는다"
|
||||
```bash label="[lab host] 수명 두 값을 읽는다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get realms/keycloak-patterns --fields accessTokenLifespan,ssoSessionIdleTimeout
|
||||
```
|
||||
@@ -630,7 +646,7 @@ IdP 는 로그인 경로의 단일 장애점이지 이미 로그인한 사용자
|
||||
|
||||
**행동** — 주입 전 절차와 같은 명령이다.
|
||||
|
||||
```bash label="[kc-lab-1] 세션을 비우고 캐시를 버린다"
|
||||
```bash label="[lab host] 세션을 비우고 캐시를 버린다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "delete from offline_client_session" -c "delete from offline_user_session"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli flushall
|
||||
@@ -656,14 +672,14 @@ kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s
|
||||
|
||||
**지우기 전에 백업 파일이 쓸 만한지 본다.** 이 파일은 이 편이 만들지 않는다 — B-7 이 Grafana Ingress 를 걷어내기 **전에** 떠 둔다. 지금은 그 Ingress 가 이미 없으니 파일이 비어 있으면 다시 뜰 원본도 없고, 되살리는 절차는 가이드에 없다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ⓪ 백업 파일이 쓸 만한지 본다"
|
||||
```bash label="[lab host] ⓪ 백업 파일이 쓸 만한지 본다"
|
||||
wc -l ~/grafana-ingress-backup.yaml
|
||||
grep -c 'app2.hyeonworks.com' ~/grafana-ingress-backup.yaml
|
||||
```
|
||||
|
||||
줄 수가 나오고 둘째 줄이 `0` 이 아니면 그 파일로 돌려줄 수 있다. `No such file or directory` 가 나오면 아래 `delete` 를 치지 않는다. B-4 로 app2 를 빌린 적이 있으면 그 편은 같은 백업을 `/tmp/grafana-ingress-backup.yaml` 에 떠 두므로 그쪽을 본다.
|
||||
|
||||
```bash label="[kc-lab-1] C-2 를 이어서 하지 않을 때만 친다"
|
||||
```bash label="[lab host] C-2 를 이어서 하지 않을 때만 친다"
|
||||
kubectl -n keycloak-lab delete ingress oauth2-proxy
|
||||
kubectl apply -f ~/grafana-ingress-backup.yaml
|
||||
curl -sI https://app2.hyeonworks.com/ | head -3
|
||||
@@ -703,6 +719,14 @@ curl -sI https://app2.hyeonworks.com/ | head -3
|
||||
| 스크린샷 두 장이 똑같다 | 실제로 같은 파일이다. 조작이 아니다 | 구별은 터미널 출력이 한다 |
|
||||
| `kubectl exec keycloak-0 -- curl` 이 `exit 127` | Keycloak 이미지에 curl 도 wget 도 없다 | 밖에서 치거나 임시 curl 파드 |
|
||||
|
||||
## 이 실험대에서 아직 못 밟은 단계
|
||||
|
||||
2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.**
|
||||
|
||||
- **남은 것** — app1·app2 로그인과 두 앱의 Redis 키 대조. 지금까지 밟은 것 — 세션 정리 구간 전부(`logout-all` 이 안 듣는 것, 자식 1633·부모 6, 롤아웃이 kcadm 세션을 날리는 것).
|
||||
- **막는 것** — `https://auth.hyeonworks.com` 이 서지 않는다. 와일드카드 인증서(Cloudflare API 토큰이 필요한 DNS-01)와, 밖에서 실험대에 닿는 길(호스트의 libvirt `guest_input` 구멍 — A-4 에서 확인한 `ExecStartPost` 누락)이 둘 다 있어야 한다.
|
||||
- **그때까지 이 편의 실측 가운데 `(observed)` 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다.** 나머지는 원래 실행의 값이다.
|
||||
|
||||
## 무엇이 관측이고 무엇이 아닌가
|
||||
|
||||
이 절차의 숫자는 `2026-09-04 14:44–14:48 KST` 에 돈 한 번의 실행에서 나왔다(observed).
|
||||
|
||||
+42
-25
@@ -41,7 +41,9 @@ IdP 쪽에만 백채널 로그아웃 URL 을 넣고 로그아웃을 건 뒤, Red
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 `kc-lab-1` 에서 `kubectl` 로 친다. `kubectl` 에 `sudo` 를 붙이지 않는다. 앱 소스를 뒤지는 `grep -rn` 한 단계만 저장소를 체크아웃해 둔 워크스테이션에서 치고, 밖에서 후보 경로를 두드리는 `curl` 은 개발 머신에서 친다. 도달성을 재는 두 줄은 클러스터 안에 띄운 임시 파드 안에서 친다.
|
||||
명령은 `[lab host]` 에서 `kubectl` 로 친다. `kubectl` 에 `sudo` 를 붙이지 않는다. 앱 소스를 뒤지는 `grep -rn` 한 단계만 저장소를 체크아웃해 둔 워크스테이션에서 치고, 밖에서 후보 경로를 두드리는 `curl` 은 개발 머신에서 친다. 도달성을 재는 두 줄은 클러스터 안에 띄운 임시 파드 안에서 친다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
C-1 이 세운 두 앱을 그대로 쓴다. app1 은 BFF(Backend for Frontend, 브라우저 대신 토큰을 들고 있는 백엔드)이고 app2 는 oauth2-proxy 다.
|
||||
|
||||
@@ -111,7 +113,7 @@ C-1 은 왜 안 퍼졌는지를 안 물었다. 후보가 셋 있고 각각 판
|
||||
|
||||
되돌리기는 백업한 값으로 다시 `update` 하는 것이고, 백업이 `{ }` 처럼 비어 있었다면 빈 객체로 되돌린다. 아래 한 줄이 그 빈 객체 갈래다 — 백업에 값이 있었던 사람이 그 값을 다시 넣는 명령은 가이드에 없다(unknown). 그쪽은 복구 1 절에서 다시 짚는다. `$CID` 는 주입 1 절에서 잡는 클라이언트 UUID 이므로, 여기를 먼저 읽는 지금은 아직 비어 있다. 실제로 칠 때는 그 절의 `CID=` 를 친 다음이다.
|
||||
|
||||
```bash label="[kc-lab-1] 클라이언트 속성을 되돌린다 — $CID 를 잡은 뒤에 친다"
|
||||
```bash label="[lab host] 클라이언트 속성을 되돌린다 — $CID 를 잡은 뒤에 친다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
update "clients/$CID" -r keycloak-patterns -s 'attributes={}'
|
||||
```
|
||||
@@ -132,7 +134,7 @@ IdP 설정 → 앱 소스 → 앱의 실제 경로 → ★ 끊을 세션이 있
|
||||
|
||||
**행동** — 관리자 비밀번호를 Secret 에서 읽어 명령 치환으로 넘긴다.
|
||||
|
||||
```bash label="[kc-lab-1] 비밀번호를 화면에 찍지 않고 로그인한다"
|
||||
```bash label="[lab host] 비밀번호를 화면에 찍지 않고 로그인한다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
config credentials --server http://localhost:8080 --realm master --user admin \
|
||||
--password "$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
@@ -151,14 +153,27 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
**행동** — 두 클라이언트를 각각 본다.
|
||||
|
||||
```bash label="[kc-lab-1] 속성을 통째로 받아 눈으로 훑는다"
|
||||
```bash label="[lab host] 속성을 통째로 받아 눈으로 훑는다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get clients -r keycloak-patterns -q clientId=oauth2-proxy --fields attributes
|
||||
```
|
||||
|
||||
**예상 결과** — 실측은 이렇다(observed, `01-current-state.txt`).
|
||||
**예상 결과** — 2026-09-17 에도 세 경로가 전부 `302` 였고 `Location` 이 로그인 시작점을 가리켰다(observed).
|
||||
|
||||
```text
|
||||
HTTP/1.1 302 Found
|
||||
Location: http://app1.hyeonworks.com/oauth2/authorization/keycloak
|
||||
|
||||
/logout/connect/back-channel/keycloak 302
|
||||
/backchannel-logout 302
|
||||
/oauth2/sign_out 302
|
||||
```
|
||||
|
||||
세 후보 어느 것도 백채널 엔드포인트가 아니다. **`302` 는 「그 경로가 있다」가 아니라 「인증이 필요한 요청으로 처리됐다」**를 뜻한다. 그런 핸들러가 없어 기본 규칙에 걸렸다. `Location` 이 로그인 시작점을 가리켜 그것을 증명한다.
|
||||
|
||||
원래 실행의 실측은 이렇다(observed, `01-current-state.txt`).
|
||||
|
||||
```text
|
||||
=== 현재 클라이언트의 백채널 로그아웃 설정 ===
|
||||
@@ -201,6 +216,8 @@ grep -rn "oidcLogout\|backchannel" bff/src/main/java/
|
||||
|
||||
**왜 필요한가** — 소스에 없으니 경로도 없다. 다만 빈 출력에는 원인이 둘이므로 다음 단계에서 배포된 앱을 직접 두드린다.
|
||||
|
||||
2026-09-17 에 같은 저장소에서 다시 쳤다(observed). `bff/src/main/java/` 는 있는데 출력이 비었다 — **경로를 잘못 짚은 것이 아니라 정말 없다.**
|
||||
|
||||
**문제가 생기면** — `grep` 이 빈 출력을 줄 때는 경로가 맞는지 먼저 의심한다. `ls bff/src/main/java/` 로 디렉터리가 실재하는지 본다. 없는 디렉터리를 뒤져도 `grep` 은 조용히 0건을 준다.
|
||||
|
||||
### 3. 배포된 앱의 실제 경로를 본다
|
||||
@@ -246,7 +263,7 @@ done
|
||||
|
||||
**행동** — C-1 에서 배운 대로 realm 을 조인해서 센다.
|
||||
|
||||
```bash label="[kc-lab-1] ① realm 을 조인해 세고 Redis 도 본다"
|
||||
```bash label="[lab host] ① realm 을 조인해 세고 Redis 도 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from offline_user_session us join realm r on r.id=us.realm_id
|
||||
where r.name='keycloak-patterns' and us.offline_flag='0'"
|
||||
@@ -265,7 +282,7 @@ IdP 세션이 0 이다. Redis 에는 키가 2개 있는데 Keycloak 쪽은 비
|
||||
|
||||
**행동** — 그러니 세션을 만든다. 브라우저에서 `https://app1.hyeonworks.com/` 을 열고 `labuser` 로 로그인한다. 가이드는 이 줄에 계정 이름과 비밀번호를 나란히 적지만 여기에는 이름만 옮긴다. 그 비밀번호는 B-0 의 `set-password` 로 따라 하는 사람이 정하는 값이다. 그리고 다시 센다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 똑같은 두 줄을 다시 친다"
|
||||
```bash label="[lab host] ② 똑같은 두 줄을 다시 친다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from offline_user_session us join realm r on r.id=us.realm_id
|
||||
where r.name='keycloak-patterns' and us.offline_flag='0'"
|
||||
@@ -296,7 +313,7 @@ kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '*'
|
||||
|
||||
**행동** — 클라이언트 UUID 를 먼저 잡아 눈으로 확인하고, 지금 속성을 파일로 받는다.
|
||||
|
||||
```bash label="[kc-lab-1] UUID 를 잡고 속성을 파일로 받는다"
|
||||
```bash label="[lab host] UUID 를 잡고 속성을 파일로 받는다"
|
||||
CID=$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get clients -r keycloak-patterns -q clientId=bff-confidential --fields id \
|
||||
--format csv --noquotes | tail -1)
|
||||
@@ -328,7 +345,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
**행동** — 점 표기로 한 번 쳐 본다.
|
||||
|
||||
```bash label="[kc-lab-1] 점 표기 — 실패한다"
|
||||
```bash label="[lab host] 점 표기 — 실패한다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
update "clients/$CID" -r keycloak-patterns \
|
||||
-s "attributes.backchannel.logout.url=https://app1.hyeonworks.com/logout/connect/back-channel/keycloak"
|
||||
@@ -350,7 +367,7 @@ command terminated with exit code 1
|
||||
|
||||
**행동** — 두 속성을 한 JSON 으로 준다.
|
||||
|
||||
```bash label="[kc-lab-1] JSON 으로 통째로 교체한다"
|
||||
```bash label="[lab host] JSON 으로 통째로 교체한다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
update "clients/$CID" -r keycloak-patterns \
|
||||
-s 'attributes={"backchannel.logout.url":"https://app1.hyeonworks.com/logout/connect/back-channel/keycloak",
|
||||
@@ -369,7 +386,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
### 1. 두 속성이 둘 다 들어갔는가
|
||||
|
||||
```bash label="[kc-lab-1] 주입 전에 친 것과 똑같은 줄이다"
|
||||
```bash label="[lab host] 주입 전에 친 것과 똑같은 줄이다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes
|
||||
```
|
||||
@@ -391,7 +408,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
### 2. 이 시점의 Redis 키 이름을 적어 둔다
|
||||
|
||||
```bash label="[kc-lab-1] 키 이름을 그대로 적어 둔다"
|
||||
```bash label="[lab host] 키 이름을 그대로 적어 둔다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '*'
|
||||
```
|
||||
|
||||
@@ -416,7 +433,7 @@ kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '*'
|
||||
|
||||
**행동** — 시각을 남기고, 사용자 id 를 잡아 눈으로 확인하고, 로그아웃을 건다.
|
||||
|
||||
```bash label="[kc-lab-1] 시각을 남기고 사용자 단위로 끊는다"
|
||||
```bash label="[lab host] 시각을 남기고 사용자 단위로 끊는다"
|
||||
date '+%H:%M:%S 로그아웃'
|
||||
USERID=$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get users -r keycloak-patterns -q username=labuser --fields id \
|
||||
@@ -440,7 +457,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
### 2. IdP 세션이 끊겼는지 먼저 본다
|
||||
|
||||
```bash label="[kc-lab-1] realm 을 조인해 센다"
|
||||
```bash label="[lab host] realm 을 조인해 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from offline_user_session us join realm r on r.id=us.realm_id
|
||||
where r.name='keycloak-patterns' and us.offline_flag='0'"
|
||||
@@ -457,7 +474,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
### 3. 앱 세션을 주입 검증과 같은 명령으로 본다
|
||||
|
||||
```bash label="[kc-lab-1] 앞에서 친 것과 똑같은 줄이다"
|
||||
```bash label="[lab host] 앞에서 친 것과 똑같은 줄이다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '*'
|
||||
```
|
||||
|
||||
@@ -483,7 +500,7 @@ kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '*'
|
||||
|
||||
Keycloak 양쪽 노드와 앱 쪽이다.
|
||||
|
||||
```bash label="[kc-lab-1] ① Keycloak 두 노드를 센다"
|
||||
```bash label="[lab host] ① Keycloak 두 노드를 센다"
|
||||
kubectl -n keycloak-lab logs keycloak-0 | grep -ci backchannel
|
||||
kubectl -n keycloak-lab logs keycloak-1 | grep -ci backchannel
|
||||
```
|
||||
@@ -496,7 +513,7 @@ kubectl -n keycloak-lab logs keycloak-1 | grep -ci backchannel
|
||||
keycloak-1: 0 줄
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② BFF 쪽 도착 흔적을 본다"
|
||||
```bash label="[lab host] ② BFF 쪽 도착 흔적을 본다"
|
||||
kubectl -n keycloak-lab logs -l app=bff --since=5m --prefix | grep -i 'back-channel\|logout'
|
||||
```
|
||||
|
||||
@@ -526,7 +543,7 @@ kubectl -n keycloak-lab logs -l app=bff --since=5m --prefix | grep -i 'back-chan
|
||||
|
||||
**행동** — Keycloak 파드에는 `curl` 이 없으므로 같은 네임스페이스에 임시 파드를 띄운다. 가이드가 미검증으로 표시한 줄이고 원래 실행의 명령 원문은 기록에 없다(unknown). 출력은 실측이다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 임시 파드를 띄우고 들어간다"
|
||||
```bash label="[lab host] ① 임시 파드를 띄우고 들어간다"
|
||||
kubectl -n keycloak-lab run c2probe --rm -it --restart=Never \
|
||||
--image=curlimages/curl:8.11.1 --command -- sh
|
||||
```
|
||||
@@ -640,7 +657,7 @@ exit
|
||||
|
||||
**행동** — 백업을 먼저 읽고, 읽은 값으로 되돌린다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 백업에 무엇이 있었는지 본다"
|
||||
```bash label="[lab host] ① 백업에 무엇이 있었는지 본다"
|
||||
cat ~/c2-bff-attributes-backup.json
|
||||
```
|
||||
|
||||
@@ -648,7 +665,7 @@ cat ~/c2-bff-attributes-backup.json
|
||||
|
||||
**값이 있었으면 여기서 멈춘다.** 그 값을 다시 넣는 명령은 원본 가이드에 없다(unknown). 지어내지 않으므로 이 절차에도 없다. 주입 3 절의 `attributes=` 가 통째로 교체했으니 원래 속성은 ①이 찍어 낸 `~/c2-bff-attributes-backup.json` 안에만 있다. 그 파일을 지우지 않는다 — 되돌릴 명령을 구하면 그 파일이 있어야 쓸 수 있다. 그리고 ②를 치지 않는다. `attributes={}` 는 지금 들어 있는 것과 함께 원래 값까지 비운다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 빈 객체로 되돌리는 경우 — 백업이 비어 있었을 때만 친다"
|
||||
```bash label="[lab host] ② 빈 객체로 되돌리는 경우 — 백업이 비어 있었을 때만 친다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
update "clients/$CID" -r keycloak-patterns -s 'attributes={}'
|
||||
```
|
||||
@@ -661,7 +678,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
### 2. 세션을 정리한다
|
||||
|
||||
```bash label="[kc-lab-1] C-1 과 같은 네 줄이다"
|
||||
```bash label="[lab host] C-1 과 같은 네 줄이다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "delete from offline_client_session" -c "delete from offline_user_session"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli flushall
|
||||
@@ -679,14 +696,14 @@ kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s
|
||||
|
||||
**지우기 전에 백업 파일이 쓸 만한지 본다.** 이 파일은 이 편이 만들지 않는다 — B-7 이 Grafana Ingress 를 걷어내기 **전에** 떠 둔다. 지금은 그 Ingress 가 이미 없으니 파일이 비어 있으면 다시 뜰 원본도 없고, 되살리는 절차는 가이드에 없다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ⓪ 백업 파일이 쓸 만한지 본다"
|
||||
```bash label="[lab host] ⓪ 백업 파일이 쓸 만한지 본다"
|
||||
wc -l ~/grafana-ingress-backup.yaml
|
||||
grep -c 'app2.hyeonworks.com' ~/grafana-ingress-backup.yaml
|
||||
```
|
||||
|
||||
줄 수가 나오고 둘째 줄이 `0` 이 아니면 그 파일로 돌려줄 수 있다. `No such file or directory` 가 나오면 아래 `delete` 를 치지 않는다. B-4 로 app2 를 빌린 적이 있으면 그 편은 같은 백업을 `/tmp/grafana-ingress-backup.yaml` 에 떠 두므로 그쪽을 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 빌린 것을 걷고 백업을 올린다"
|
||||
```bash label="[lab host] ① 빌린 것을 걷고 백업을 올린다"
|
||||
kubectl -n keycloak-lab delete ingress oauth2-proxy
|
||||
kubectl apply -f ~/grafana-ingress-backup.yaml
|
||||
curl -sI https://app2.hyeonworks.com/ | head -3
|
||||
@@ -698,7 +715,7 @@ curl -sI https://app2.hyeonworks.com/ | head -3
|
||||
|
||||
**문제가 생기면** — ⓪이 `No such file or directory` 를 냈다면 `delete` 를 치기 전이므로 Grafana Ingress 는 아직 없고 oauth2-proxy 것은 아직 있다. B-7 의 백업 단계를 다시 읽고, 두 경로에 다 없으면 Ingress 를 손대지 않은 채로 두고 여기서 멈춘다. oauth2-proxy 배포까지 걷어내려면 한 줄이 더 있다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 배포까지 걷어낼 때만 친다"
|
||||
```bash label="[lab host] ② 배포까지 걷어낼 때만 친다"
|
||||
kubectl delete -f deploy/lab/k8s/b7-oauth2-proxy.yaml
|
||||
```
|
||||
|
||||
@@ -708,7 +725,7 @@ kubectl delete -f deploy/lab/k8s/b7-oauth2-proxy.yaml
|
||||
|
||||
먼저 아래 확인표의 `kubectl -n keycloak-lab get pod c2probe` 로 남았는지 보고, `NotFound` 면 더 할 것이 없다. `--rm` 으로 안 지워졌으면 직접 지운다.
|
||||
|
||||
```bash label="[kc-lab-1] 남아 있으면 지운다"
|
||||
```bash label="[lab host] 남아 있으면 지운다"
|
||||
kubectl -n keycloak-lab delete pod c2probe --ignore-not-found
|
||||
```
|
||||
|
||||
|
||||
+92
-28
@@ -40,18 +40,35 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령이 두 기계에 나뉜다. 소스를 고치고 이미지를 만드는 일은 워크스테이션에서 하고, 클러스터를 보고 배포하는 일은 `kc-lab-1` 에서 `kubectl` 과 `kcadm` 으로 한다. 그래서 코드블록마다 어디서 치는지를 붙여 두었다.
|
||||
명령이 두 기계에 나뉜다. 소스를 고치고 이미지를 만드는 일은 워크스테이션에서 하고, 클러스터를 보고 배포하는 일은 `[lab host]` 에서 `kubectl` 과 `kcadm` 으로 한다. 그래서 코드블록마다 어디서 치는지를 붙여 두었다.
|
||||
|
||||
**`[kc-lab-1]` 라벨이 붙은 블록은 게스트 셸이다.** lab host 에서 `ssh kc-lab-1` 로 들어가서 치고, 끝나면 `exit` 로 나온다. `ssh kc-lab-2 '…'` 한 줄 형태는 lab host 에서 그대로 쳐도 되므로 `[lab host]` 로 두었다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
**시작 전에 셋을 스스로 정해 둔다. 그 명령이 원 가이드에 없다(unknown).**
|
||||
|
||||
첫째, 워크스테이션에서 `kc-lab-1` 로 건너가는 명령이 이 절차에 없다. 라벨은 `[워크스테이션]` 과 `[kc-lab-1]` 을 여섯 번 오가는데 `ssh` 로 들어가는 줄도 `exit` 도 안 나온다. 경로는 이미지를 밀어 넣는 줄 하나에만 드러난다 — `ssh test-server "ssh kc-lab-1 '...'"`, 워크스테이션에서 `kc-lab-1` 까지 `test-server` 를 거친다. `[워크스테이션]` 블록은 처음 시작한 셸에서 치고 `[kc-lab-1]` 블록은 그 기계에 붙은 셸에서 친다.
|
||||
첫째, 워크스테이션에서 클러스터를 보는 기계로 건너가는 명령이 이 절차에 없다. 원 가이드의 라벨은 `[워크스테이션]` 과 `[kc-lab-1]` 을 여섯 번 오가는데 `ssh` 로 들어가는 줄도 `exit` 도 안 나온다. 경로는 이미지를 밀어 넣는 줄 하나에만 드러난다 — `ssh test-server "ssh kc-lab-1 '...'"`, 워크스테이션에서 `kc-lab-1` 까지 `test-server` 를 거친다. **`kubectl` 블록은 `kc-lab-1` 이 아니라 그 중간의 `test-server`, 즉 `[lab host]` 에서 친다** — 기반 가이드가 kubeconfig 를 거기에만 두기 때문이고, 까닭은 아래에 적었다. 그래서 건너갈 기계는 하나이고 `ssh test-server` 한 줄이면 된다. 이미지를 노드로 밀어 넣는 줄만 거기서 한 번 더 `ssh kc-lab-1` 로 들어간다.
|
||||
|
||||
둘째, 이 절차의 파일 경로가 전부 저장소 상대경로다 — `bff/pom.xml`, `deploy/lab/k8s/bff-redis.yaml`. 어느 디렉터리에서 치는지 정하는 줄이 없으므로 두 기계 각각에서 저장소 루트로 먼저 옮겨 두고 시작한다. 다른 디렉터리에서 치면 `kubectl apply` 가 경로를 못 찾고 끝난다.
|
||||
|
||||
셋째, 워크스테이션에서 고친 파일을 `kc-lab-1` 로 옮기는 단계가 없다. `vim deploy/lab/k8s/bff-redis.yaml` 은 `[워크스테이션]` 이고 그 파일을 읽는 `kubectl apply -f deploy/lab/k8s/bff-redis.yaml` 은 `[kc-lab-1]` 인데, 사이에 파일을 넘기는 명령이 원 가이드에 없다. **안 옮기고 치면 오류가 안 난다** — 손 안 댄 매니페스트가 그대로 적용돼 `SPRING_SESSION_STORE_TYPE=redis` 와 `BFF_DB_*` 가 살아 있는 채로 배포되고, 화면에는 배포 성공만 뜬다. 어긋난 것은 관찰 절의 빈 수가 `321` 이 아닌 다른 숫자로 나올 때 비로소 보인다.
|
||||
셋째, 워크스테이션에서 고친 파일을 클러스터를 보는 기계로 옮기는 단계가 없다. `vim deploy/lab/k8s/bff-redis.yaml` 은 `[워크스테이션]` 이고 그 파일을 읽는 `kubectl apply -f deploy/lab/k8s/bff-redis.yaml` 은 `[lab host]` 인데, 사이에 파일을 넘기는 명령이 원 가이드에 없다. **안 옮기고 치면 오류가 안 난다** — 손 안 댄 매니페스트가 그대로 적용돼 `SPRING_SESSION_STORE_TYPE=redis` 와 `BFF_DB_*` 가 살아 있는 채로 배포되고, 화면에는 배포 성공만 뜬다. 어긋난 것은 관찰 절의 빈 수가 `321` 이 아닌 다른 숫자로 나올 때 비로소 보인다.
|
||||
|
||||
`kubectl` 에 `sudo` 를 붙이지 않는다. root 홈에는 `~/.kube/config` 가 없어 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝난다. 반입한 B층 아홉 편의 전제 한 줄만 옛 형태로 `sudo kubectl` 을 적고 있고, 본문 명령 블록에는 한 번도 쓰지 않는다.
|
||||
|
||||
**그 규칙은 `[lab host]` 의 규칙이지 `kc-lab-1` 의 규칙이 아니다.** 기반 가이드는 kubeconfig 를 lab host 의 `~/.kube/config` 에만 두고 게스트의 사용자 홈에는 일부러 두지 않는다. 그래서 `kc-lab-1` 에서는 `sudo` 없는 `kubectl` 이 도리어 막힌다 — 2026-09-17 에 양쪽에서 쳐서 확인했다(observed).
|
||||
|
||||
| 어디서 | `kubectl …` | `sudo kubectl …` |
|
||||
|---|---|---|
|
||||
| lab host | 된다 | 안 된다 |
|
||||
| `kc-lab-1` | 안 된다 | 된다 |
|
||||
|
||||
`kc-lab-1` 에서 `sudo` 없이 치면 `connection refused` 가 아니라 이렇게 끝난다.
|
||||
|
||||
```text
|
||||
error: error loading config file "/etc/rancher/k3s/k3s.yaml": open /etc/rancher/k3s/k3s.yaml: permission denied
|
||||
```
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
| 네임스페이스 | `keycloak-lab` |
|
||||
@@ -105,7 +122,7 @@ git checkout -- bff/pom.xml bff/src/main/resources/application.yml \
|
||||
deploy/lab/k8s/bff-redis.yaml
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] 배포한 것을 통째로 지운다"
|
||||
```bash label="[lab host] 배포한 것을 통째로 지운다"
|
||||
kubectl delete -f deploy/lab/k8s/bff-redis.yaml
|
||||
```
|
||||
|
||||
@@ -123,7 +140,7 @@ PVC 는 `delete -f` 로 같이 지워진다. Redis 데이터도 함께 사라진
|
||||
|
||||
**무엇을 보는가** — 두 노드의 메모리와 CPU 여유.
|
||||
|
||||
```bash label="[kc-lab-1] 메모리와 노드 사용률을 본다"
|
||||
```bash label="[lab host] 메모리와 노드 사용률을 본다"
|
||||
free -m
|
||||
kubectl top nodes
|
||||
```
|
||||
@@ -144,11 +161,11 @@ kc-lab-2 121m 6% 1324Mi 33%
|
||||
|
||||
**무엇을 보는가** — 지금 무엇이 떠 있는지.
|
||||
|
||||
```bash label="[kc-lab-1] ① 워크로드를 본다"
|
||||
```bash label="[lab host] ① 워크로드를 본다"
|
||||
kubectl -n keycloak-lab get all
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② Secret 과 Ingress 는 따로 본다"
|
||||
```bash label="[lab host] ② Secret 과 Ingress 는 따로 본다"
|
||||
kubectl -n keycloak-lab get secret,ingress
|
||||
```
|
||||
|
||||
@@ -162,7 +179,7 @@ kubectl -n keycloak-lab get secret,ingress
|
||||
|
||||
관리 자격증명을 잡는다. `kcadm` 은 Keycloak 이미지 안에 있다.
|
||||
|
||||
```bash label="[kc-lab-1] ① kcadm 에 로그인한다"
|
||||
```bash label="[lab host] ① kcadm 에 로그인한다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
config credentials --server http://localhost:8080 --realm master --user admin \
|
||||
--password "$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
@@ -171,28 +188,37 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
비밀번호를 화면에 찍지 않는다. 명령 치환으로 넘기므로 값이 터미널에도 셸 히스토리에도 남지 않는다. 길이만 센다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 비밀번호의 길이만 센다"
|
||||
```bash label="[lab host] ② 비밀번호의 길이만 센다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
|
||||
```
|
||||
|
||||
realm 과 클라이언트를 만든다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ realm 과 confidential 클라이언트를 만든다"
|
||||
```bash label="[lab host] ③ realm 과 confidential 클라이언트를 만든다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
create realms -s realm=keycloak-patterns -s enabled=true -s accessTokenLifespan=60
|
||||
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
create clients -r keycloak-patterns \
|
||||
-s clientId=bff-confidential -s publicClient=false -s secret=bff-lab-secret \
|
||||
-s directAccessGrantsEnabled=true \
|
||||
-s 'redirectUris=["https://app1.hyeonworks.com/*"]'
|
||||
```
|
||||
|
||||
**`directAccessGrantsEnabled=true` 가 붙는 까닭은 B-3 때문이다.** 이 줄이 없으면 클라이언트는 기본값 `false` 로 만들어지고, B-3 이 같은 클라이언트로 `grant_type=password` 를 칠 때 이렇게 끝난다(2026-09-17, observed).
|
||||
|
||||
```text
|
||||
{"error":"unauthorized_client","error_description":"Client not allowed for direct access grants"}
|
||||
```
|
||||
|
||||
B-0 의 브라우저 로그인은 인가 코드 흐름이라 이 값이 `false` 여도 되고, 그래서 **B-0 을 끝까지 밟아도 안 드러난다.** 나중에 B-3 에서 막히고, 그때는 원인이 B-0 에 있다는 것이 안 보인다.
|
||||
|
||||
`-s secret=bff-lab-secret` 은 Keycloak 쪽에 저장되는 값이고, BFF 가 실제로 보내는 값은 배포 매니페스트가 만드는 Secret `bff-secrets` 의 `KEYCLOAK_CLIENT_SECRET` 이다. 둘이 같아야 로그인이 끝까지 간다. 이 절차에는 둘을 견주는 단계가 없으므로, 주입 절에서 `deploy/lab/k8s/bff-redis.yaml` 을 열었을 때 그 칸을 눈으로 확인한다.
|
||||
|
||||
만들어진 값을 되읽는다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ realm 설정 세 칸만 뽑아 본다"
|
||||
```bash label="[lab host] ④ realm 설정 세 칸만 뽑아 본다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get realms/keycloak-patterns --fields realm,enabled,accessTokenLifespan
|
||||
```
|
||||
@@ -209,17 +235,36 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
사용자를 만든다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 사용자를 만든다"
|
||||
```bash label="[lab host] ① 사용자를 만든다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
create users -r keycloak-patterns -s username=labuser -s enabled=true
|
||||
```
|
||||
|
||||
비밀번호를 준다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 비밀번호를 준다"
|
||||
```bash label="[lab host] ② 비밀번호를 준다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
set-password -r keycloak-patterns --username labuser --new-password 'lab-user-change-me'
|
||||
```
|
||||
|
||||
```bash label="[lab host] ②b 계정을 완전하게 만든다 — 안 하면 B-3 에서 막힌다"
|
||||
U=$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get users -r keycloak-patterns -q username=labuser --fields id --format csv --noquotes | tr -d '\r')
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
update users/$U -r keycloak-patterns \
|
||||
-s emailVerified=true -s email=labuser@example.invalid \
|
||||
-s firstName=Lab -s lastName=User -s 'requiredActions=[]'
|
||||
```
|
||||
|
||||
**①②만 치면 계정이 「완전히 설정되지 않은」 상태로 남는다.** `create users` 는 `emailVerified` 를 `false` 로 두고 이름과 이메일 칸을 비운 채 만든다. 그 상태로 B-3 이 `grant_type=password` 를 치면 이렇게 끝난다(2026-09-17, observed).
|
||||
|
||||
```text
|
||||
{"error":"invalid_grant","error_description":"Account is not fully set up"}
|
||||
```
|
||||
|
||||
**`$U` 를 쓸 때 변수 이름을 `UID` 로 짓지 않는다.** zsh 에서 `UID` 는 읽기 전용 숫자 변수라 `bad math expression` 으로 끝난다(2026-09-17, observed). 위처럼 `U` 같은 다른 이름을 쓴다.
|
||||
|
||||
②b 까지 치면 같은 요청이 토큰을 돌려준다(observed).
|
||||
**★ 뒤의 편들이 이 값을 그대로 쓴다.** 이 실험대는 `labpass` 를 썼고, B-3 과 B-6 의 토큰
|
||||
요청이 `-d password=labpass` 로 그 값을 박아 놓고 있다. **여기서 다른 값을 정했으면 그
|
||||
자리들도 같이 바꿔야 한다** — 안 바꾸면 B-3 의 첫 토큰 요청이 `401` 로 떨어지고, 그것이
|
||||
@@ -231,7 +276,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
**문제가 생기면** — realm 을 통째로 지우면 이 단계가 만든 것이 함께 사라진다.
|
||||
|
||||
```bash label="[kc-lab-1] realm 을 통째로 지운다"
|
||||
```bash label="[lab host] realm 을 통째로 지운다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
delete realms/keycloak-patterns
|
||||
```
|
||||
@@ -368,7 +413,7 @@ org.yaml.snakeyaml.constructor.SafeConstructor.processDuplicateKeys
|
||||
|
||||
매니페스트를 적용하고 롤아웃이 끝날 때까지 기다린다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 적용하고 두 롤아웃을 기다린다"
|
||||
```bash label="[lab host] ① 적용하고 두 롤아웃을 기다린다"
|
||||
kubectl apply -f deploy/lab/k8s/bff-redis.yaml
|
||||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
kubectl -n keycloak-lab rollout status deployment/bff --timeout=300s
|
||||
@@ -425,7 +470,7 @@ token-uri: ${KC_ISSUER_INTERNAL}/protocol/openid-connect/token # BFF
|
||||
|
||||
### 파드 두 개가 서로 다른 노드에 떴는가
|
||||
|
||||
```bash label="[kc-lab-1] BFF 와 Redis 의 배치를 본다"
|
||||
```bash label="[lab host] BFF 와 Redis 의 배치를 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide -l app=bff
|
||||
kubectl -n keycloak-lab get pods -o wide -l app=redis
|
||||
```
|
||||
@@ -442,7 +487,7 @@ BFF 두 개가 서로 다른 노드에 있어야 한다. `topologySpreadConstrai
|
||||
|
||||
### 외부 진입점이 이 애플리케이션의 HTML 을 주는가
|
||||
|
||||
```bash label="[kc-lab-1] 상태 줄과 헤더를 읽는 형태로 친다"
|
||||
```bash label="[lab host] 상태 줄과 헤더를 읽는 형태로 친다"
|
||||
curl -I https://app1.hyeonworks.com/
|
||||
```
|
||||
|
||||
@@ -470,14 +515,14 @@ Bad Gateway
|
||||
|
||||
그래서 파드 안에서 직접 받는다. alpine 기반 JRE 이미지에는 `wget` 이 들어 있어서 Keycloak 이미지와 달리 파드 안에서 HTTP 요청을 보낼 수 있다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 받을 파드 이름을 잡는다"
|
||||
```bash label="[lab host] ① 받을 파드 이름을 잡는다"
|
||||
kubectl -n keycloak-lab get pods -l app=bff
|
||||
BFF=$(kubectl -n keycloak-lab get pod -l app=bff \
|
||||
--field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')
|
||||
echo "$BFF"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드 안에서 받아 크기를 센다"
|
||||
```bash label="[lab host] ② 파드 안에서 받아 크기를 센다"
|
||||
kubectl -n keycloak-lab exec "$BFF" -- \
|
||||
wget -qO- http://localhost:8083/actuator/beans > /tmp/beans.json
|
||||
wc -c /tmp/beans.json
|
||||
@@ -491,7 +536,7 @@ wc -c /tmp/beans.json
|
||||
|
||||
`0` 이면 못 받은 것이고 몇 백 바이트면 로그인 페이지나 오류 본문이다. 앞부분을 열어 확정한다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 앞 200바이트만 본다"
|
||||
```bash label="[lab host] ③ 앞 200바이트만 본다"
|
||||
head -c 200 /tmp/beans.json ; echo
|
||||
```
|
||||
|
||||
@@ -511,13 +556,13 @@ head -c 200 /tmp/beans.json ; echo
|
||||
"이름":{"aliases":[],"scope":"singleton","type":"패키지.클래스", ...}
|
||||
```
|
||||
|
||||
이 실험대는 `jq` 가 없어 `grep` 으로 덩어리를 뽑았다. 원 가이드가 아래 두 줄을 미검증으로 표시했다(unknown).
|
||||
이 실험대는 `jq` 가 없어 `grep` 으로 덩어리를 뽑았다. **아래 두 줄은 2026-09-17 에 쳐서 돌았다**(observed) — 그때까지 미검증이던 형태다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 빈의 개수를 센다"
|
||||
```bash label="[lab host] ① 빈의 개수를 센다"
|
||||
grep -o '"aliases":\[' /tmp/beans.json | wc -l
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 이름과 타입을 한 줄로 뽑아 거른다"
|
||||
```bash label="[lab host] ② 이름과 타입을 한 줄로 뽑아 거른다"
|
||||
grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"' /tmp/beans.json \
|
||||
| sed 's/{"aliases".*"type":"/ -> /' \
|
||||
| grep -i authorizedclient
|
||||
@@ -527,6 +572,17 @@ grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"
|
||||
|
||||
**어디를 보나** — ② 의 출력은 이렇게 생겼다(observed).
|
||||
|
||||
2026-09-17 에는 저장소를 **붙인 상태**(B-1·B-2 를 거친 매니페스트)에서 쳐서 이렇게 나왔다(observed).
|
||||
|
||||
```text
|
||||
빈 개수: 437
|
||||
authorizedClientService -> org.springframework.security.oauth2.client.JdbcOAuth2AuthorizedClientService
|
||||
authorizedClientRepository -> …AuthenticatedPrincipalOAuth2AuthorizedClientRepository
|
||||
RedisSessionConfiguration -> org.springframework.boot.autoconfigure.session.RedisSessionConfiguration
|
||||
```
|
||||
|
||||
**`321` 이 아니라 `437` 인 것이 이 측정이 쓸모 있다는 증거다.** B-0 의 `321` 은 저장소를 하나도 안 붙였을 때의 수이고, Redis 세션과 JDBC 토큰 저장소가 붙으면 빈이 그만큼 늘어난다. 이 편을 B-0 상태에서 재려면 위 「주입」 절의 되돌리기를 먼저 끝내야 한다 — **빈 수 하나로 지금 어느 상태인지 알 수 있다.**
|
||||
|
||||
```text
|
||||
"authorizedClientManager" -> org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager
|
||||
"authorizedClientRepository" -> org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository
|
||||
@@ -554,7 +610,7 @@ grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"
|
||||
|
||||
없다는 것은 세어서 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ Redis · Spring Session 계열 빈을 센다"
|
||||
```bash label="[lab host] ③ Redis · Spring Session 계열 빈을 센다"
|
||||
grep -ci 'RedisSessionRepository\|SpringHttpSessionConfiguration\|LettuceConnectionFactory' /tmp/beans.json
|
||||
```
|
||||
|
||||
@@ -598,7 +654,7 @@ https://app1.hyeonworks.com/login?error
|
||||
|
||||
로그를 본다.
|
||||
|
||||
```bash label="[kc-lab-1] 두 replica 의 로그를 접두사와 함께 본다"
|
||||
```bash label="[lab host] 두 replica 의 로그를 접두사와 함께 본다"
|
||||
kubectl -n keycloak-lab logs -l app=bff --tail=100 --prefix
|
||||
```
|
||||
|
||||
@@ -619,7 +675,7 @@ kubectl -n keycloak-lab logs -l app=bff --tail=100 --prefix
|
||||
|
||||
replica 를 하나로 줄인다.
|
||||
|
||||
```bash label="[kc-lab-1] ① replica 를 1 로 줄인다"
|
||||
```bash label="[lab host] ① replica 를 1 로 줄인다"
|
||||
kubectl -n keycloak-lab scale deployment/bff --replicas=1
|
||||
kubectl -n keycloak-lab rollout status deployment/bff --timeout=180s
|
||||
kubectl -n keycloak-lab get pods -l app=bff
|
||||
@@ -664,7 +720,7 @@ kubectl -n keycloak-lab get pods -l app=bff
|
||||
|
||||
### 1. replica 를 되돌린다
|
||||
|
||||
```bash label="[kc-lab-1] replica 를 2 로 올린다"
|
||||
```bash label="[lab host] replica 를 2 로 올린다"
|
||||
kubectl -n keycloak-lab scale deployment/bff --replicas=2
|
||||
kubectl -n keycloak-lab rollout status deployment/bff --timeout=180s
|
||||
```
|
||||
@@ -686,7 +742,7 @@ git status --short
|
||||
|
||||
**B-1 이나 B-2 로 이어서 갈 것이면 이 절을 치지 않는다.** B-1 은 「Redis 는 배포만 되어 있고 아직 연결되지 않았다. B-0 이 그렇게 만들어 뒀다」를 전제로 시작하고 B-2 는 그 위에서 시작한다. 아래 두 줄은 `bff` 와 `redis` Deployment 를 PVC 까지, realm `keycloak-patterns` 를 `labuser` 까지 한꺼번에 없앤다. 치고 나면 B-1 은 배포와 realm 과 사용자를 다시 만드는 데서 시작해야 하는데 그 순서는 B-1 에 안 적혀 있고 이 편의 주입 절과 realm·사용자 단계로 되돌아와야 한다. B층을 여기서 끝낼 때만 친다.
|
||||
|
||||
```bash label="[kc-lab-1] 배포와 realm 을 지운다"
|
||||
```bash label="[lab host] 배포와 realm 을 지운다"
|
||||
kubectl delete -f deploy/lab/k8s/bff-redis.yaml
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
delete realms/keycloak-patterns
|
||||
@@ -748,6 +804,14 @@ authorization-uri: ${KC_ISSUER_EXTERNAL}/protocol/openid-connect/auth
|
||||
authorization-uri: ${KC_ISSUER_EXTERNAL:http://localhost:8080/realms/keycloak-patterns}/protocol/openid-connect/auth
|
||||
```
|
||||
|
||||
## 이 실험대에서 아직 못 밟은 단계
|
||||
|
||||
2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.**
|
||||
|
||||
- **남은 것** — 브라우저 로그인(3절 뒤 전부)과 그 로그인이 만드는 `/actuator/beans` 재측정. 지금까지 밟은 것 — 이미지 빌드·반입, realm·클라이언트·사용자 생성, 매니페스트 적용, 빈 437개 확인.
|
||||
- **막는 것** — `https://auth.hyeonworks.com` 이 서지 않는다. 와일드카드 인증서(Cloudflare API 토큰이 필요한 DNS-01)와, 밖에서 실험대에 닿는 길(호스트의 libvirt `guest_input` 구멍 — A-4 에서 확인한 `ExecStartPost` 누락)이 둘 다 있어야 한다.
|
||||
- **그때까지 이 편의 실측 가운데 `(observed)` 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다.** 나머지는 원래 실행의 값이다.
|
||||
|
||||
## 무엇이 관측이고 무엇이 아닌가
|
||||
|
||||
이 절차의 숫자는 `2026-09-04 13:39–13:46 KST` 에 돈 한 번의 실행에서 나왔다(observed).
|
||||
|
||||
+39
-29
@@ -41,7 +41,9 @@ Redis 를 세션 저장소로 붙이고 B-0 에서 찍어 둔 빈 목록과 견
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
B-0 과 같다. 소스를 고치고 이미지를 만드는 일은 워크스테이션에서 하고, 클러스터를 보고 배포하는 일은 `kc-lab-1` 에서 친다. `kubectl` 에 `sudo` 를 붙이지 않는다. 브라우저 창도 하나 열어 둔다.
|
||||
B-0 과 같다. 소스를 고치고 이미지를 만드는 일은 워크스테이션에서 하고, 클러스터를 보고 배포하는 일은 `[lab host]` 에서 친다. `kubectl` 에 `sudo` 를 붙이지 않는다. 브라우저 창도 하나 열어 둔다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
**끊기는 곳도 B-0 과 같다. 시작 전에 셋을 스스로 정해 둔다 — 그 명령이 원 가이드에 없다(unknown).**
|
||||
|
||||
@@ -115,7 +117,7 @@ BFF 가 돌고 있나 → B-0 의 답 세 개 → Redis 가 비어 있나 →
|
||||
|
||||
**무엇을 보는가** — 파드 배치.
|
||||
|
||||
```bash label="[kc-lab-1] BFF 와 Redis 의 배치를 본다"
|
||||
```bash label="[lab host] BFF 와 Redis 의 배치를 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide -l app=bff
|
||||
kubectl -n keycloak-lab get pods -o wide -l app=redis
|
||||
```
|
||||
@@ -134,7 +136,7 @@ redis-568bd7c4-5c5vc 1/1 Running 0 20m 10.42.1.53 kc-lab-2
|
||||
|
||||
**무엇을 보는가** — 주입 뒤에 견줄 빈 수와 빈 이름 셋.
|
||||
|
||||
```bash label="[kc-lab-1] ① 빈 목록을 파드 안에서 받는다"
|
||||
```bash label="[lab host] ① 빈 목록을 파드 안에서 받는다"
|
||||
BFF=$(kubectl -n keycloak-lab get pod -l app=bff \
|
||||
--field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')
|
||||
kubectl -n keycloak-lab exec "$BFF" -- \
|
||||
@@ -144,7 +146,7 @@ wc -c /tmp/beans-before.json
|
||||
|
||||
이 실험대는 `jq` 가 없어 `grep` 으로 덩어리를 뽑았다. 원 가이드가 아래 두 줄을 미검증으로 표시했다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② 빈을 세고 저장소 계열 이름을 뽑는다"
|
||||
```bash label="[lab host] ② 빈을 세고 저장소 계열 이름을 뽑는다"
|
||||
grep -o '"aliases":\[' /tmp/beans-before.json | wc -l
|
||||
grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"' /tmp/beans-before.json \
|
||||
| sed 's/{"aliases".*"type":"/ -> /' \
|
||||
@@ -174,12 +176,12 @@ grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"
|
||||
|
||||
**무엇을 보는가** — 연결 여부와 키 개수.
|
||||
|
||||
```bash label="[kc-lab-1] ① 응답과 판 번호를 읽는 형태로 본다"
|
||||
```bash label="[lab host] ① 응답과 판 번호를 읽는 형태로 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli ping
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli info server | head
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 키 개수와 키 이름을 본다"
|
||||
```bash label="[lab host] ② 키 개수와 키 이름을 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan
|
||||
```
|
||||
@@ -205,11 +207,11 @@ redis_version:7.4.x
|
||||
|
||||
한 번은 통째로 보고 그다음 걸러 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 환경변수를 통째로 본다"
|
||||
```bash label="[lab host] ① 환경변수를 통째로 본다"
|
||||
kubectl -n keycloak-lab exec "$BFF" -- printenv | sort
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② Redis 쪽만 거른다"
|
||||
```bash label="[lab host] ② Redis 쪽만 거른다"
|
||||
kubectl -n keycloak-lab exec "$BFF" -- printenv | grep -i redis
|
||||
```
|
||||
|
||||
@@ -339,13 +341,13 @@ kubectl -n keycloak-lab rollout restart deployment/bff
|
||||
|
||||
지금 멈추려면 롤아웃을 되돌린다.
|
||||
|
||||
```bash label="[kc-lab-1] 중간에 그만둘 때 치는 한 줄"
|
||||
```bash label="[lab host] 중간에 그만둘 때 치는 한 줄"
|
||||
kubectl -n keycloak-lab rollout undo deployment/bff
|
||||
```
|
||||
|
||||
**예상 결과** — 파드가 뜨지 않는다. 넓은 것부터 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 파드 상태를 본다"
|
||||
```bash label="[lab host] ③ 파드 상태를 본다"
|
||||
kubectl -n keycloak-lab get pods -l app=bff
|
||||
```
|
||||
|
||||
@@ -356,11 +358,11 @@ bff-695646ddb-kzs9k 0/1 CrashLoopBackOff 3 (20s ago) 90s
|
||||
|
||||
로그보다 먼저 이벤트를 보고, 그다음 로그를 본다. 지금 파드와 죽기 전 파드를 따로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 이벤트를 본다"
|
||||
```bash label="[lab host] ④ 이벤트를 본다"
|
||||
kubectl -n keycloak-lab describe pod -l app=bff | tail -20
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ⑤ 지금 로그와 죽기 전 로그를 본다"
|
||||
```bash label="[lab host] ⑤ 지금 로그와 죽기 전 로그를 본다"
|
||||
kubectl -n keycloak-lab logs -l app=bff --tail=40
|
||||
kubectl -n keycloak-lab logs -l app=bff --previous --tail=40
|
||||
```
|
||||
@@ -377,7 +379,7 @@ Failed to bind properties under 'spring.data.redis.port' to int:
|
||||
|
||||
**왜 필요한가** — 마지막 줄의 `"tcp://10.43.57.116:6379"` 는 매니페스트 어디에도 쓰지 않은 값이다. 주입 전에 `printenv` 로 미리 본 그 환경변수이고 쿠버네티스가 넣었다. Redis 는 멀쩡하고, 파드는 Redis 에 붙어 보지도 못한 채 설정 바인딩에서 죽었다. 메시지가 `Failed to bind properties` 라고 말하고 있다.
|
||||
|
||||
```bash label="[kc-lab-1] ⑥ Redis 가 멀쩡한지 따로 확인한다"
|
||||
```bash label="[lab host] ⑥ Redis 가 멀쩡한지 따로 확인한다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli ping
|
||||
```
|
||||
|
||||
@@ -414,7 +416,7 @@ spec:
|
||||
value: "6379"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 적용하고 롤아웃을 기다린다"
|
||||
```bash label="[lab host] ② 적용하고 롤아웃을 기다린다"
|
||||
kubectl apply -f deploy/lab/k8s/bff-redis.yaml
|
||||
kubectl -n keycloak-lab rollout status deployment/bff --timeout=300s
|
||||
```
|
||||
@@ -441,7 +443,7 @@ bff-695646ddb-vjqzf true kc-lab-2
|
||||
|
||||
### 1. 자동 주입된 환경변수가 사라졌는가
|
||||
|
||||
```bash label="[kc-lab-1] 새 파드 이름을 다시 잡고 환경변수를 본다"
|
||||
```bash label="[lab host] 새 파드 이름을 다시 잡고 환경변수를 본다"
|
||||
BFF=$(kubectl -n keycloak-lab get pod -l app=bff \
|
||||
--field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')
|
||||
kubectl -n keycloak-lab exec "$BFF" -- printenv | grep -i redis
|
||||
@@ -458,7 +460,7 @@ REDIS_PORT=6379
|
||||
|
||||
### 2. 자동구성이 실제로 걸렸는가
|
||||
|
||||
```bash label="[kc-lab-1] 파드 배치와 헬스를 본다"
|
||||
```bash label="[lab host] 파드 배치와 헬스를 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide -l app=bff
|
||||
kubectl -n keycloak-lab exec "$BFF" -- \
|
||||
wget -qO- http://localhost:8083/actuator/health
|
||||
@@ -486,7 +488,7 @@ kubectl -n keycloak-lab exec "$BFF" -- \
|
||||
|
||||
B-0 의 방법을 그대로 다시 쓴다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 빈 목록을 받고 개수를 센다"
|
||||
```bash label="[lab host] ① 빈 목록을 받고 개수를 센다"
|
||||
kubectl -n keycloak-lab exec "$BFF" -- \
|
||||
wget -qO- http://localhost:8083/actuator/beans > /tmp/beans-after.json
|
||||
grep -o '"aliases":\[' /tmp/beans-after.json | wc -l
|
||||
@@ -500,7 +502,7 @@ grep -o '"aliases":\[' /tmp/beans-after.json | wc -l
|
||||
|
||||
새로 생긴 세션 저장소 빈을 뽑는다. 원 가이드가 아래 줄을 미검증으로 표시했다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② 세션·Redis 계열 빈을 뽑는다"
|
||||
```bash label="[lab host] ② 세션·Redis 계열 빈을 뽑는다"
|
||||
grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"' /tmp/beans-after.json \
|
||||
| sed 's/{"aliases".*"type":"/ -> /' \
|
||||
| grep -iE 'session|redis'
|
||||
@@ -518,7 +520,7 @@ grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"
|
||||
|
||||
같은 파일에서 인가된 클라이언트 쪽을 따로 뽑는다. 이 실험이 판정하려는 것이 이쪽이다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ authorized client 계열을 뽑는다"
|
||||
```bash label="[lab host] ③ authorized client 계열을 뽑는다"
|
||||
grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"' /tmp/beans-after.json \
|
||||
| sed 's/{"aliases".*"type":"/ -> /' \
|
||||
| grep -i authorizedclient
|
||||
@@ -550,7 +552,7 @@ grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"
|
||||
|
||||
**무엇을 보는가** — 옮겨진 것 안에 무엇이 들었는지.
|
||||
|
||||
```bash label="[kc-lab-1] ① 키 개수와 키 이름을 본다"
|
||||
```bash label="[lab host] ① 키 개수와 키 이름을 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan
|
||||
```
|
||||
@@ -567,7 +569,7 @@ bff:session:sessions:8963b6de-3564-4775-9ccd-1ee9616b83ae
|
||||
|
||||
키 이름을 변수로 잡는다. 이 줄에는 걸러 내는 조각이 둘 붙어 있다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 세션 키 하나를 변수에 담는다"
|
||||
```bash label="[lab host] ② 세션 키 하나를 변수에 담는다"
|
||||
KEY=$(kubectl -n keycloak-lab exec deploy/redis -- \
|
||||
redis-cli --scan --pattern 'bff:session:sessions:*' | grep -v expires | head -1 | tr -d '\r')
|
||||
echo "$KEY"
|
||||
@@ -577,7 +579,7 @@ echo "$KEY"
|
||||
|
||||
바로 위 `redis-cli --scan` 이 이미 전체 키를 보여 줬으므로 그 출력에서 키 하나를 눈으로 골라 쳐도 되는데, 원 가이드가 그 두 단계 형태를 적어 두지 않았다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ③ 타입과 필드 이름을 본다"
|
||||
```bash label="[lab host] ③ 타입과 필드 이름을 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli type "$KEY"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli hkeys "$KEY"
|
||||
```
|
||||
@@ -599,7 +601,7 @@ kubectl -n keycloak-lab exec deploy/redis -- redis-cli hkeys "$KEY"
|
||||
|
||||
### 3. TTL 과 값의 바이트를 본다
|
||||
|
||||
```bash label="[kc-lab-1] ① 남은 수명을 본다"
|
||||
```bash label="[lab host] ① 남은 수명을 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli ttl "$KEY"
|
||||
```
|
||||
|
||||
@@ -610,7 +612,7 @@ kubectl -n keycloak-lab exec deploy/redis -- redis-cli ttl "$KEY"
|
||||
|
||||
`spring.session.timeout=30m` 인 1800초에서 방금 지난 만큼 줄어든 값이다. 세션 TTL 1772초와 access token 수명 60초가 처음부터 어긋나 있다. 어느 쪽에 맞출지 고르기 전에 이미 어긋나 있고, 그 간극을 누가 메우는지가 B-3 의 주제다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 값의 앞 네 줄을 이스케이프해서 본다"
|
||||
```bash label="[lab host] ② 값의 앞 네 줄을 이스케이프해서 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --no-raw hgetall "$KEY" | head -4
|
||||
```
|
||||
|
||||
@@ -633,7 +635,7 @@ D-2 의 버전 업그레이드에서 이 성질이 다시 나온다. Spring Secu
|
||||
|
||||
**목적** — 롤링 재시작으로 Redis 덕을 보는지 확인한다. 롤링 재시작은 정상 작업이라 되돌릴 것이 없다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 파드를 전부 교체한다"
|
||||
```bash label="[lab host] ① 파드를 전부 교체한다"
|
||||
kubectl -n keycloak-lab rollout restart deployment/bff
|
||||
kubectl -n keycloak-lab rollout status deployment/bff --timeout=300s
|
||||
```
|
||||
@@ -710,7 +712,7 @@ kubectl -n keycloak-lab rollout status deployment/bff --timeout=300s
|
||||
|
||||
Redis 를 비우는 것은 되돌릴 수 없다. 지운 세션은 돌아오지 않고 로그인한 사용자는 전부 로그아웃된다. 실험대라서 하는 일이다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 개수를 보고 비우고 다시 센다"
|
||||
```bash label="[lab host] ① 개수를 보고 비우고 다시 센다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli flushdb
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||||
@@ -718,7 +720,7 @@ kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||||
|
||||
세션 하나만 지우려면 이쪽이다. `$KEY` 는 관찰 2번에서 담아 둔 키이고, 그 뒤로 `rollout restart` 와 브라우저 왕복이 들어가 절차가 40분쯤 걸리므로 터미널을 새로 열었으면 값이 비어 있다. 치기 전에 `echo "$KEY"` 로 키가 나오는지 보고, 안 나오면 관찰 2번의 `KEY=$(...)` 두 줄을 다시 쳐서 담는다. 바로 위 ① 의 `flushdb` 를 이미 쳤으면 지울 키가 없으므로 ② 를 건너뛴다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 잡아 둔 키 하나만 지운다"
|
||||
```bash label="[lab host] ② 잡아 둔 키 하나만 지운다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli del "$KEY"
|
||||
```
|
||||
|
||||
@@ -760,7 +762,7 @@ kubectl -n keycloak-lab exec deploy/redis -- redis-cli del "$KEY"
|
||||
|
||||
아래 줄의 `$BFF` 는 주입 검증 1번에서 담아 둔 파드 이름이다. 「막히면」은 아무 때나 펼치는 절이고 그 사이에 관찰 4번의 `rollout restart` 가 파드를 통째로 갈아치우므로, 그대로 치면 `NotFound` 가 온다. 치기 전에 주입 검증 1번의 `BFF=$(...)` 두 줄을 다시 쳐서 이름을 새로 담는다.
|
||||
|
||||
```bash label="[kc-lab-1] 예외만 골라 본다"
|
||||
```bash label="[lab host] 예외만 골라 본다"
|
||||
kubectl -n keycloak-lab logs "$BFF" --tail=100 | grep -iE 'exception|error'
|
||||
```
|
||||
|
||||
@@ -776,10 +778,18 @@ java.nio.channels.UnresolvedAddressException
|
||||
value: http://echo.header-lab.svc:8081
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] 그 서비스가 어디 있는지 본다"
|
||||
```bash label="[lab host] 그 서비스가 어디 있는지 본다"
|
||||
kubectl -n header-lab get svc echo
|
||||
```
|
||||
|
||||
## 이 실험대에서 아직 못 밟은 단계
|
||||
|
||||
2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.**
|
||||
|
||||
- **남은 것** — 브라우저 로그인 뒤의 Redis 키·필드·TTL 관찰. 지금까지 밟은 것 — 이미지 빌드·반입, 배포, `/actuator/beans` 로 `RedisSessionConfiguration` 확인.
|
||||
- **막는 것** — `https://auth.hyeonworks.com` 이 서지 않는다. 와일드카드 인증서(Cloudflare API 토큰이 필요한 DNS-01)와, 밖에서 실험대에 닿는 길(호스트의 libvirt `guest_input` 구멍 — A-4 에서 확인한 `ExecStartPost` 누락)이 둘 다 있어야 한다.
|
||||
- **그때까지 이 편의 실측 가운데 `(observed)` 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다.** 나머지는 원래 실행의 값이다.
|
||||
|
||||
## 무엇이 관측이고 무엇이 아닌가
|
||||
|
||||
이 절차의 숫자는 `2026-09-04 13:59–14:03 KST` 에 돈 한 번의 실행에서 나왔다(observed).
|
||||
|
||||
+49
-26
@@ -47,13 +47,28 @@ B-1 이 Redis 로 안 옮긴 토큰을 PostgreSQL 로 옮겨 보는 절차다. `
|
||||
|
||||
**B-0 과 B-1 을 글자 그대로 따라 친 사람은 그 상태가 아니다.** B-0 의 주입 절이 `spring-boot-starter-jdbc` · `postgresql` · `h2` 와 `SecurityConfig` 의 명시 빈 둘과 `spring.datasource` 와 `BFF_DB_*` 를 **지우고**, B-1 은 그것을 되살리지 않는다. 그래서 B-0 → B-1 → B-2 순으로 온 BFF 에는 `JdbcOAuth2AuthorizedClientService` 가 없다.
|
||||
|
||||
**2026-09-17 에 그 상태를 실제로 봤다**(observed). 저장소의 매니페스트를 그대로 배포하면 표는 생기고 행은 0 이다.
|
||||
|
||||
```text
|
||||
Table "public.oauth2_authorized_client"
|
||||
client_registration_id | character varying(100) | not null
|
||||
principal_name | character varying(200) | not null
|
||||
access_token_value | bytea | not null
|
||||
…
|
||||
select count(*) from oauth2_authorized_client → 0
|
||||
```
|
||||
|
||||
`\d` 가 정의를 돌려주므로 「배선이 됐다」로 읽기 쉬운데, 행은 브라우저 로그인이 한 번 끝나야 생긴다.
|
||||
|
||||
**그 상태로 쳐도 화면은 정상으로 보인다.** 주입 전 3번이 표를 만들고 `\d oauth2_authorized_client` 도 정의를 돌려주는데, 행만 한 번도 안 생긴다. 그러면 주입 전 6번의 「이 값이 아직 `false` 로 나오면 … 로그아웃하고 다시 로그인한다」가 끝나지 않는 고리가 되어 로그인만 반복하게 된다. 두 번 재로그인해도 `accessTokenStoredOnServer` 가 `false` 이고 표의 행이 0이면 배선이 안 들어간 상태이므로 이 절차를 멈춘다.
|
||||
|
||||
무엇을 넣어야 하는지는 B-0 의 주입 절이 지울 목록으로 적어 두었고, 바로 아래 「전제와 되돌리기」가 그 넷을 표로 옮겨 두었다. 그 넷을 되돌려 넣는 순서와 그때 친 빌드 명령은 원 가이드에 없다(unknown).
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 `kc-lab-1` 에서 `kubectl` 로 친다. `kubectl` 에 `sudo` 를 붙이지 않는다. 브라우저에서 버튼을 누르고 터미널에서 저장소를 세는 왕복이 이 절차의 대부분이라 터미널 하나와 브라우저 창 하나를 나란히 둔다. 로그아웃 한 단계만 브라우저 개발자 도구의 콘솔에서 친다.
|
||||
명령은 `[lab host]` 에서 `kubectl` 로 친다. `kubectl` 에 `sudo` 를 붙이지 않는다. 브라우저에서 버튼을 누르고 터미널에서 저장소를 세는 왕복이 이 절차의 대부분이라 터미널 하나와 브라우저 창 하나를 나란히 둔다. 로그아웃 한 단계만 브라우저 개발자 도구의 콘솔에서 친다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
예외는 아래 「전제와 되돌리기」의 두 블록뿐이다. 거기서만 워크스테이션의 저장소를 고치고 이미지를 다시 만든다. **그 기계로 건너가는 명령은 이 절차에 없다** — 경로는 이미지를 밀어 넣는 줄 하나에만 드러난다(`ssh test-server "ssh kc-lab-1 '...'"`, 워크스테이션에서 `test-server` 를 거쳐 `kc-lab-1` 로 간다). 그 블록의 `bff/pom.xml` 과 `deploy/lab/k8s/bff-redis.yaml` 도 저장소 상대경로라 어느 디렉터리에서 치는지 적힌 줄이 없으므로, 두 기계 각각에서 저장소 루트로 옮겨 두고 시작한다. 이 셋의 명령이 원 가이드에 없다(unknown).
|
||||
|
||||
@@ -155,7 +170,7 @@ B-2 자신의 가이드에는 JDBC 배선을 넣는 편집 명령도 그때 친
|
||||
|
||||
**무엇을 보는가** — 파드 배치와 재시작 횟수.
|
||||
|
||||
```bash label="[kc-lab-1] 네임스페이스 전체를 본다"
|
||||
```bash label="[lab host] 네임스페이스 전체를 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
|
||||
@@ -173,7 +188,7 @@ redis-... 1/1 Running 0 3d ... kc-lab-2
|
||||
|
||||
파드 이름은 자주 바뀌므로 이름 대신 라벨로 부른다.
|
||||
|
||||
```bash label="[kc-lab-1] 라벨로 BFF 만 본다"
|
||||
```bash label="[lab host] 라벨로 BFF 만 본다"
|
||||
kubectl -n keycloak-lab get pods -l app=bff
|
||||
```
|
||||
|
||||
@@ -192,7 +207,7 @@ bff-555df79c97-vgg6g 1/1 Running 0 22s
|
||||
|
||||
**무엇을 보는가** — `oauth2_authorized_client` 가 있는지. 원래 실행은 여기서 한 번 넘어졌다. 파드는 떴고 Hikari 도 붙었는데 테이블이 없었고 아무도 그것을 신고하지 않았다.
|
||||
|
||||
```bash label="[kc-lab-1] 테이블 정의를 물어본다"
|
||||
```bash label="[lab host] 테이블 정의를 물어본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||
psql -U keycloak -d keycloak -c '\d oauth2_authorized_client'
|
||||
```
|
||||
@@ -222,7 +237,7 @@ command terminated with exit code 1
|
||||
|
||||
DDL 은 여러 줄이고 나중에 다시 쓸 것이므로 파일로 만든다. 터미널에 붙여 넣는 명령과 프로그램 원문을 섞지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 편집기로 DDL 파일을 만든다"
|
||||
```bash label="[lab host] ① 편집기로 DDL 파일을 만든다"
|
||||
vim /tmp/oauth2-pg.sql
|
||||
```
|
||||
|
||||
@@ -246,7 +261,7 @@ CREATE TABLE oauth2_authorized_client (
|
||||
|
||||
위 DDL 은 `02-schema.txt` 의 `=== PostgreSQL 전용 스키마 ===` 절 원문이다(observed).
|
||||
|
||||
```bash label="[kc-lab-1] ② 파일을 파드 안으로 넘겨 태운다"
|
||||
```bash label="[lab host] ② 파일을 파드 안으로 넘겨 태운다"
|
||||
kubectl -n keycloak-lab exec -i deploy/postgres -- \
|
||||
psql -U keycloak -d keycloak < /tmp/oauth2-pg.sql
|
||||
```
|
||||
@@ -262,7 +277,7 @@ CREATE TABLE
|
||||
|
||||
**문제가 생기면** — 되돌리는 명령은 있지만 평소에는 치지 않는다. B-3 이후로도 이 표를 계속 쓴다.
|
||||
|
||||
```bash label="[kc-lab-1] 표를 지운다. 평소에는 치지 않는다"
|
||||
```bash label="[lab host] 표를 지운다. 평소에는 치지 않는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||
psql -U keycloak -d keycloak -c 'drop table oauth2_authorized_client'
|
||||
```
|
||||
@@ -271,7 +286,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||
|
||||
**무엇을 보는가** — 이 편의 답이 박혀 있는 한 줄. 이 줄을 보기 전에는 다음으로 넘어가지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] 테이블 정의를 다시 물어본다"
|
||||
```bash label="[lab host] 테이블 정의를 다시 물어본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||
psql -U keycloak -d keycloak -c '\d oauth2_authorized_client'
|
||||
```
|
||||
@@ -310,7 +325,7 @@ PRIMARY KEY, btree (client_registration_id, principal_name)
|
||||
|
||||
**무엇을 보는가** — 관찰 절에서 로그아웃 전후로 견줄 숫자 셋. 다른 명령으로 재면 비교가 아니다.
|
||||
|
||||
```bash label="[kc-lab-1] ① BFF 세션 키를 접두어로 골라 본다"
|
||||
```bash label="[lab host] ① BFF 세션 키를 접두어로 골라 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern 'bff:session:*'
|
||||
```
|
||||
|
||||
@@ -322,12 +337,12 @@ bff:session:sessions:8963b6de-3564-4775-9ccd-1ee9616b83ae
|
||||
|
||||
`KEYS *` 대신 `--scan` 을 쓰는 것은 `KEYS` 가 Redis 를 잡아 두고 전 키를 훑기 때문이다. 그리고 `dbsize` 는 이 실험에서 부정확하다. Redis 하나를 BFF 와 B-7 의 oauth2-proxy 가 나눠 쓰므로 `dbsize` 에는 `_oauth2_proxy-…` 키도 섞인다. 접두어로 걸러 세는 쪽이 맞다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 토큰 행 수를 센다"
|
||||
```bash label="[lab host] ② 토큰 행 수를 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||
psql -U keycloak -d keycloak -c 'select count(*) from oauth2_authorized_client'
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ③ Keycloak 세션을 DB 쪽에서 센다"
|
||||
```bash label="[lab host] ③ Keycloak 세션을 DB 쪽에서 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||
psql -U keycloak -d keycloak \
|
||||
-c 'select offline_flag, count(*) from offline_user_session group by 1'
|
||||
@@ -335,7 +350,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||
|
||||
온라인 세션도 `offline_user_session` 에 `offline_flag = 0` 으로 들어 있다. B-3 에서 확인된 성질이다. 원래 실행은 Keycloak 관리 API 로 셌고 증거에는 숫자만 남아 있다. ③ 은 같은 숫자를 DB 쪽에서 보는 형태이고 원 가이드가 미검증으로 표시했다(unknown). 관리 API 로 보려면 이쪽이다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 관리 API 로 세는 형태"
|
||||
```bash label="[lab host] ④ 관리 API 로 세는 형태"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
config credentials --server http://localhost:8080 --realm master --user admin \
|
||||
--password "$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
@@ -367,7 +382,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
**무엇을 보는가** — 덮어쓰이기 전의 행 수와 토큰 해시와 발급 시각.
|
||||
|
||||
```bash label="[kc-lab-1] ① 행 수와 토큰 해시와 발급 시각을 함께 본다"
|
||||
```bash label="[lab host] ① 행 수와 토큰 해시와 발급 시각을 함께 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"select client_registration_id, principal_name, access_token_issued_at,
|
||||
md5(access_token_value) as at_md5
|
||||
@@ -390,7 +405,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c
|
||||
|
||||
크기도 같이 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 두 토큰의 바이트 수를 본다"
|
||||
```bash label="[lab host] ② 두 토큰의 바이트 수를 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"select client_registration_id, principal_name, access_token_type,
|
||||
length(access_token_value) as at_len, length(refresh_token_value) as rt_len
|
||||
@@ -423,7 +438,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c
|
||||
|
||||
지우기 전에 무엇을 지울지 눈으로 본다. 이 Redis 는 BFF 혼자 쓰는 것이 아니다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 전체 키를 한 번 본다"
|
||||
```bash label="[lab host] ① 전체 키를 한 번 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan
|
||||
```
|
||||
|
||||
@@ -439,7 +454,7 @@ _oauth2_proxy-f6a9201fd534a047998278452001ccbf
|
||||
|
||||
원래 실행은 스크립트를 돌렸고 아래 형태는 원 가이드가 손으로 치기 좋게 고쳐 미검증으로 표시한 것이다(unknown). 후속 문서 3절이 oauth2-proxy 세션을 지울 때 쓴 것과 같은 모양이다.
|
||||
|
||||
```bash label="[kc-lab-1] ② BFF 세션만 골라 지우고 시각을 남긴다"
|
||||
```bash label="[lab host] ② BFF 세션만 골라 지우고 시각을 남긴다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern 'bff:session:*' \
|
||||
| xargs -r kubectl -n keycloak-lab exec deploy/redis -- redis-cli del
|
||||
date '+%H:%M:%S 세션 삭제'
|
||||
@@ -466,7 +481,7 @@ date '+%H:%M:%S 세션 삭제'
|
||||
|
||||
### 1. BFF 세션만 사라지고 B-7 키는 그대로인가
|
||||
|
||||
```bash label="[kc-lab-1] 두 접두어를 따로 센다"
|
||||
```bash label="[lab host] 두 접두어를 따로 센다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern 'bff:session:*'
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '_oauth2_proxy-*'
|
||||
```
|
||||
@@ -475,7 +490,7 @@ kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern '_oauth2
|
||||
|
||||
### 2. BFF 가 재시작되지 않았는가
|
||||
|
||||
```bash label="[kc-lab-1] 재시작 횟수를 본다"
|
||||
```bash label="[lab host] 재시작 횟수를 본다"
|
||||
kubectl -n keycloak-lab get pods -l app=bff
|
||||
```
|
||||
|
||||
@@ -491,7 +506,7 @@ kubectl -n keycloak-lab get pods -l app=bff
|
||||
|
||||
**무엇을 보는가** — 주입 전에 친 것과 똑같은 명령의 결과.
|
||||
|
||||
```bash label="[kc-lab-1] 대조군과 같은 질의를 다시 친다"
|
||||
```bash label="[lab host] 대조군과 같은 질의를 다시 친다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"select client_registration_id, principal_name, access_token_issued_at,
|
||||
md5(access_token_value) as at_md5
|
||||
@@ -559,7 +574,7 @@ A 쪽에서 다음 요청을 하면 B 의 토큰을 쓰게 된다. 같은 사용
|
||||
|
||||
값을 찍기 전에 무엇을 찍게 될지 길이로 먼저 안다.
|
||||
|
||||
```bash label="[kc-lab-1] ① refresh token 의 바이트 수만 본다"
|
||||
```bash label="[lab host] ① refresh token 의 바이트 수만 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select length(refresh_token_value) from oauth2_authorized_client"
|
||||
```
|
||||
@@ -568,7 +583,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
원래 실행은 앞 200자 남짓을 통째로 찍었다. 아래 형태는 화면에 남는 양을 줄인 것이고 원 가이드가 미검증으로 표시했다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② 앞 40자만 텍스트로 디코드해 본다"
|
||||
```bash label="[lab host] ② 앞 40자만 텍스트로 디코드해 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select left(convert_from(refresh_token_value,'UTF8'), 40) from oauth2_authorized_client"
|
||||
```
|
||||
@@ -584,7 +599,7 @@ eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldU
|
||||
|
||||
정말 JWT 인지 헤더를 풀어 본다. 원 가이드는 이 줄도 미검증으로 표시한다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ③ 첫 조각만 잘라 base64 로 푼다"
|
||||
```bash label="[lab host] ③ 첫 조각만 잘라 base64 로 푼다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select convert_from(refresh_token_value,'UTF8') from oauth2_authorized_client limit 1" \
|
||||
| cut -d. -f1 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
|
||||
@@ -623,7 +638,7 @@ SyntaxError: invalid syntax
|
||||
|
||||
로그아웃 전에 세 숫자를 먼저 잡는다. 주입 전에 정해 둔 명령 그대로다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 로그아웃 전 두 숫자를 잡는다"
|
||||
```bash label="[lab host] ① 로그아웃 전 두 숫자를 잡는다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern 'bff:session:*'
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||
psql -U keycloak -d keycloak -tAc 'select count(*) from oauth2_authorized_client'
|
||||
@@ -652,7 +667,7 @@ console.log(r.status, r.url);
|
||||
|
||||
로그아웃 후 같은 세 명령을 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 로그아웃 뒤 세 숫자를 같은 명령으로 잡는다"
|
||||
```bash label="[lab host] ② 로그아웃 뒤 세 숫자를 같은 명령으로 잡는다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern 'bff:session:*'
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"select principal_name, access_token_issued_at, access_token_expires_at
|
||||
@@ -714,7 +729,7 @@ Q3 는 미지수 5번으로 「두 store 를 logout 에서 어떻게 한 번에
|
||||
|
||||
### 1. 남은 행을 지운다
|
||||
|
||||
```bash label="[kc-lab-1] 이 사용자의 행만 지운다"
|
||||
```bash label="[lab host] 이 사용자의 행만 지운다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"delete from oauth2_authorized_client where principal_name = 'labuser'"
|
||||
```
|
||||
@@ -729,7 +744,7 @@ RP 가 로그아웃을 안 보내 주므로 사람이 직접 끊는다. 브라
|
||||
https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/logout
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] 세션 수가 줄었는지 본다"
|
||||
```bash label="[lab host] 세션 수가 줄었는지 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||
psql -U keycloak -d keycloak -c 'select offline_flag, count(*) from offline_user_session group by 1'
|
||||
```
|
||||
@@ -765,6 +780,14 @@ JDBC 배선까지 걷어낼 것이면 전제와 되돌리기 절의 두 블록
|
||||
| 로그아웃했는데 다시 들어가진다 | 버그가 아니다. Keycloak SSO 세션이 살아 있다 | RP-initiated logout 을 사람이 연다 |
|
||||
| `dbsize` 와 세어 본 키 수가 다르다 | oauth2-proxy 키가 섞여 있다 | `--scan --pattern` 으로 나눠 센다 |
|
||||
|
||||
## 이 실험대에서 아직 못 밟은 단계
|
||||
|
||||
2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.**
|
||||
|
||||
- **남은 것** — 브라우저 로그인 뒤의 `oauth2_authorized_client` 행 관찰과 `accessTokenStoredOnServer` 판정. 지금까지 밟은 것 — 표가 생겨 있고 행이 `0` 인 것까지.
|
||||
- **막는 것** — `https://auth.hyeonworks.com` 이 서지 않는다. 와일드카드 인증서(Cloudflare API 토큰이 필요한 DNS-01)와, 밖에서 실험대에 닿는 길(호스트의 libvirt `guest_input` 구멍 — A-4 에서 확인한 `ExecStartPost` 누락)이 둘 다 있어야 한다.
|
||||
- **그때까지 이 편의 실측 가운데 `(observed)` 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다.** 나머지는 원래 실행의 값이다.
|
||||
|
||||
## 무엇이 관측이고 무엇이 아닌가
|
||||
|
||||
이 절차의 숫자는 `2026-09-04 14:09–14:13 KST` 에 돈 한 번의 실행에서 나왔다(observed).
|
||||
|
||||
+66
-24
@@ -37,7 +37,9 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 `kc-lab-1` 에서 `kubectl` 로 친다. 토큰을 주고받는 `curl` 만 탐침 파드 안에서 치는데, Keycloak 이미지에 `curl` 도 `wget` 도 없기 때문이다(`exit 127`). 브라우저는 필요 없다 — direct grant(`grant_type=password`)로 토큰을 만들므로 전 구간이 터미널에서 끝난다.
|
||||
명령은 `[lab host]` 에서 `kubectl` 로 친다. 토큰을 주고받는 `curl` 만 탐침 파드 안에서 치는데, Keycloak 이미지에 `curl` 도 `wget` 도 없기 때문이다(`exit 127`). 브라우저는 필요 없다 — direct grant(`grant_type=password`)로 토큰을 만들므로 전 구간이 터미널에서 끝난다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
터미널은 둘을 연다. 하나는 탐침 파드 셸을 붙잡고 있고, 다른 하나로 데이터베이스를 뒤진다. 파드 안에서 잡은 `RT` 와 `SID` 는 파드 밖으로 따라가지 않는다.
|
||||
|
||||
@@ -83,7 +85,7 @@ B-2 가 토큰을 PostgreSQL 로 옮겼고 두 replica 가 같은 행을 본다.
|
||||
|
||||
**realm 설정을 바꾸는 실험이다.** `revokeRefreshToken` 을 켜면 realm 전체에 걸리고, 같은 realm 을 쓰는 다른 작업이 영향을 받는다. B-2 의 BFF 로그인도 그 안에 든다. 실험대에서만 하고, 중간에 그만두려면 아래 한 줄이면 된다.
|
||||
|
||||
```bash label="[kc-lab-1] 중간에 그만둘 때 치는 한 줄"
|
||||
```bash label="[lab host] 중간에 그만둘 때 치는 한 줄"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
update realms/keycloak-patterns -s revokeRefreshToken=false -s refreshTokenMaxReuse=0
|
||||
```
|
||||
@@ -100,7 +102,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
**무엇을 보는가** — 파드 셋의 상태와 배치.
|
||||
|
||||
```bash label="[kc-lab-1] 파드 배치를 본다"
|
||||
```bash label="[lab host] 파드 배치를 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
|
||||
@@ -121,7 +123,7 @@ postgres-... 1/1 Running 0 5d ... kc-lab-2
|
||||
|
||||
① 파드 안에서 관리 세션을 만든다. 비밀번호는 명령 치환으로 넘기므로 값이 터미널에도 셸 히스토리에도 안 남는다.
|
||||
|
||||
```bash label="[kc-lab-1] kcadm 관리 세션을 만든다"
|
||||
```bash label="[lab host] kcadm 관리 세션을 만든다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
config credentials --server http://localhost:8080 --realm master --user admin \
|
||||
--password "$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
@@ -134,7 +136,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
**문제가 생기면** — 비밀번호가 실제로 있는지는 값이 아니라 길이로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] 비밀번호의 길이만 센다"
|
||||
```bash label="[lab host] 비밀번호의 길이만 센다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
|
||||
```
|
||||
@@ -143,7 +145,7 @@ kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
|
||||
**무엇을 보는가** — 주입이 건드릴 스위치와 건드리지 않을 값.
|
||||
|
||||
```bash label="[kc-lab-1] realm 의 세 값을 읽는다"
|
||||
```bash label="[lab host] realm 의 세 값을 읽는다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get realms/keycloak-patterns \
|
||||
--fields revokeRefreshToken,refreshTokenMaxReuse,accessTokenLifespan
|
||||
@@ -173,7 +175,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
① 파드를 띄우고 Ready 까지 기다린다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 상주 탐침 파드를 띄운다"
|
||||
```bash label="[lab host] ① 상주 탐침 파드를 띄운다"
|
||||
kubectl -n keycloak-lab run b3-probe --image=curlimages/curl:8.11.1 \
|
||||
--restart=Never \
|
||||
--env="KC=http://keycloak.keycloak-lab.svc:8080/realms/keycloak-patterns/protocol/openid-connect/token" \
|
||||
@@ -185,13 +187,39 @@ kubectl -n keycloak-lab wait --for=condition=Ready pod/b3-probe --timeout=120s
|
||||
|
||||
② 환경변수가 들어갔는지 값이 아니라 길이로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 넘어간 값의 길이를 센다"
|
||||
```bash label="[lab host] ② 넘어간 값의 길이를 센다"
|
||||
kubectl -n keycloak-lab exec b3-probe -- sh -c 'echo "KC=$KC CS길이=${#CS}"'
|
||||
```
|
||||
|
||||
**그 길이를 `15` 와 맞추려 하지 않는다.** 아래 실측의 `15` 는 그 실험대의 값이고, B-0 이 적은 `-s secret=bff-lab-secret` 을 쓰면 `14` 다(2026-09-17, observed). 볼 것은 숫자가 아니라 **매니페스트 쪽과 Keycloak 쪽이 같은가**이고, B-0 이 「이 절차에는 둘을 견주는 단계가 없다」고 적어 둔 그 대조가 이것이다. 값을 안 찍고 견주는 형태는 이렇다.
|
||||
|
||||
```bash label="[lab host] 두 값이 같은지 값을 안 찍고 본다"
|
||||
A=$(kubectl -n keycloak-lab get secret bff-secrets \
|
||||
-o jsonpath='{.data.KEYCLOAK_CLIENT_SECRET}' | base64 -d)
|
||||
C=$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get clients -r keycloak-patterns -q clientId=bff-confidential \
|
||||
--fields id --format csv --noquotes | tr -d '\r')
|
||||
S=$(kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get clients/$C/client-secret -r keycloak-patterns \
|
||||
--fields value --format csv --noquotes | tr -d '\r')
|
||||
echo "매니페스트 ${#A}자 · Keycloak ${#S}자"
|
||||
[ "$A" = "$S" ] && echo "두 값이 같다" || echo "두 값이 다르다"
|
||||
```
|
||||
|
||||
2026-09-17 실측은 `매니페스트 14자 · Keycloak 14자` 와 `두 값이 같다` 였다(observed).
|
||||
|
||||
**여기서 한 번에 막히는 것이 둘 더 있다.** 둘 다 원인이 B-0 에 있고 B-0 을 끝까지 밟아도 안 드러난다 — B-0 의 로그인은 인가 코드 흐름이라 둘 다 필요 없기 때문이다(2026-09-17, observed).
|
||||
|
||||
| 5번에서 나오는 것 | 무엇이 빠졌나 | 어디서 고치나 |
|
||||
|---|---|---|
|
||||
| `unauthorized_client` · `Client not allowed for direct access grants` | 클라이언트의 `directAccessGrantsEnabled` 가 `false` | B-0 의 클라이언트 생성에 `-s directAccessGrantsEnabled=true` |
|
||||
| `invalid_grant` · `Account is not fully set up` | `labuser` 의 `emailVerified` 가 `false` 이고 이메일·이름 칸이 비었다 | B-0 의 사용자 생성 뒤 `-s emailVerified=true -s email=… -s firstName=… -s lastName=…` |
|
||||
|
||||
둘을 고친 뒤 같은 요청이 토큰을 돌려줬다(observed).
|
||||
|
||||
③ 파드 셸로 들어간다. 프롬프트가 `/ $` 로 바뀐다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 파드 셸로 들어간다"
|
||||
```bash label="[lab host] ③ 파드 셸로 들어간다"
|
||||
kubectl -n keycloak-lab exec -it b3-probe -- sh
|
||||
```
|
||||
|
||||
@@ -205,7 +233,7 @@ KC=http://keycloak.keycloak-lab.svc:8080/realms/keycloak-patterns/protocol/openi
|
||||
|
||||
**문제가 생기면** — `CS길이=0` 이면 `--env` 가 빈 값을 넘겼다. 파드를 지우고 다시 띄운다.
|
||||
|
||||
```bash label="[kc-lab-1] 탐침 파드를 지운다"
|
||||
```bash label="[lab host] 탐침 파드를 지운다"
|
||||
kubectl -n keycloak-lab delete pod b3-probe --ignore-not-found
|
||||
```
|
||||
|
||||
@@ -273,7 +301,7 @@ done
|
||||
|
||||
**무엇을 보는가** — 정상 세션의 `client_sessions` 가 몇인가. 파드 밖에서 친다.
|
||||
|
||||
```bash label="[kc-lab-1] 정상 세션의 client session 을 센다"
|
||||
```bash label="[lab host] 정상 세션의 client session 을 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"select us.user_session_id, us.offline_flag,
|
||||
(select count(*) from offline_client_session cs
|
||||
@@ -320,7 +348,7 @@ user session 과 client session 은 서로 다르다.
|
||||
|
||||
① 회전을 켜고 시각을 남긴다.
|
||||
|
||||
```bash label="[kc-lab-1] 회전을 켜고 시각을 남긴다"
|
||||
```bash label="[lab host] 회전을 켜고 시각을 남긴다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
update realms/keycloak-patterns -s revokeRefreshToken=true -s refreshTokenMaxReuse=0
|
||||
date '+%H:%M:%S 회전 켬'
|
||||
@@ -340,7 +368,7 @@ date '+%H:%M:%S 회전 켬'
|
||||
|
||||
결과를 해석하기 전에, 주입이 의도한 것만 건드렸는지 본다. 설정은 똑같은 명령으로 다시 읽는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① realm 을 똑같은 명령으로 다시 읽는다"
|
||||
```bash label="[lab host] ① realm 을 똑같은 명령으로 다시 읽는다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get realms/keycloak-patterns \
|
||||
--fields revokeRefreshToken,refreshTokenMaxReuse,accessTokenLifespan
|
||||
@@ -354,7 +382,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
`revokeRefreshToken` 이 `true` 여야 한다. `false` 그대로면 `update` 가 다른 realm 에 갔거나 kcadm 세션이 만료됐다. kcadm 은 실패해도 조용할 때가 있어 반드시 다시 읽어서 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 재시작이 올랐는지 본다"
|
||||
```bash label="[lab host] ② 재시작이 올랐는지 본다"
|
||||
kubectl -n keycloak-lab get pods -l app=keycloak
|
||||
```
|
||||
|
||||
@@ -393,7 +421,7 @@ SID=$(echo "$R" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p' | cut -d. -f2 \
|
||||
echo "refresh=${#RT}자 SID=$SID"
|
||||
```
|
||||
|
||||
**동시에 다섯 개를 던진다.** 원래 실행은 스크립트였고, 아래는 가이드가 손으로 치기 좋게 고쳐 미검증으로 표시한 형태다(unknown). 본문과 응답 코드를 파일로 갈라 순서대로 다시 읽게 했다.
|
||||
**동시에 다섯 개를 던진다.** 원래 실행은 스크립트였고, 아래는 가이드가 손으로 치기 좋게 고친 형태다. **2026-09-17 에 그대로 쳐서 돌았다**(observed). 본문과 응답 코드를 파일로 갈라 순서대로 다시 읽게 했다.
|
||||
|
||||
```sh label="[탐침 파드] ② 같은 토큰으로 동시에 다섯 번 갱신한다"
|
||||
i=1
|
||||
@@ -437,6 +465,20 @@ done
|
||||
| `Maximum allowed refresh token reuse exceeded` | 재사용 탐지가 발동 |
|
||||
| `Session doesn't have required client` | 그 여파 — client session 이 이미 없다 |
|
||||
|
||||
**2026-09-17 실측**(observed) — 같은 모양이 다시 나왔다.
|
||||
|
||||
```text
|
||||
요청 1: HTTP 400 Session doesn't have required client
|
||||
요청 2: HTTP 400 Session doesn't have required client
|
||||
요청 3: HTTP 400 Maximum allowed refresh token reuse exceeded
|
||||
요청 4: HTTP 200 (발급됨)
|
||||
요청 5: HTTP 400 Session doesn't have required client
|
||||
이긴 요청의 새 refresh 길이: 810
|
||||
그 토큰을 다시 쓰면: 400 Session doesn't have required client
|
||||
```
|
||||
|
||||
**몇 번 요청이 이기는지는 실행마다 다르다.** 위 실측에서는 5번이었고 2026-09-17 에는 4번이었다. 판정에 쓰는 것은 번호가 아니라 **`200` 이 하나이고 그 하나가 받은 토큰도 안 통한다**는 두 가지다.
|
||||
|
||||
하나만 이기고 나머지가 진 것이라면 지는 쪽 메시지가 전부 같아야 한다. 두 종류라는 것은 중간에 상태가 바뀌었다는 뜻이다. 성공한 번호는 환경마다 다르고 증거에서는 5번이었지만 순서는 스케줄링이 정한다 — 몇 번이 이겼는가는 아무 의미가 없다.
|
||||
|
||||
**이긴 요청의 토큰을 다시 써 본다.** 여기서 진짜 답이 나온다.
|
||||
@@ -473,7 +515,7 @@ curl -s -w '\n%{http_code}\n' -X POST "$KC" \
|
||||
|
||||
**아래 두 블록에 박힌 `'BvFiB01Rntz1FcLdf7zG4BNt'` 를 자기 `SID` 로 바꾼다.** 그것은 원래 실행의 sid 라, 그대로 붙여넣으면 질의는 오류 없이 성공하고 `(0 rows)` 만 돌아온다. 두 블록 모두 바꿔야 한다 — 한쪽만 바꾸면 두 출력이 서로 다른 세션을 말한다. 출력은 마지막 줄부터 읽는다. `(1 row)` 면 그 sid 의 세션을 찾았고, `(0 rows)` 면 sid 를 안 바꿨거나 다른 값을 넣었다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 그 sid 의 세션이 남아 있는가"
|
||||
```bash label="[lab host] ④ 그 sid 의 세션이 남아 있는가"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"select us.user_session_id, us.offline_flag, us.last_session_refresh
|
||||
from offline_user_session us
|
||||
@@ -492,7 +534,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c
|
||||
|
||||
행이 있다. 세션이 통째로 지워진 것이 아니다. 그러면 왜 `Session doesn't have required client` 인가 — client session 을 센다.
|
||||
|
||||
```bash label="[kc-lab-1] ⑤ 같은 sid 의 client session 을 센다"
|
||||
```bash label="[lab host] ⑤ 같은 sid 의 client session 을 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"select us.user_session_id, us.offline_flag,
|
||||
(select count(*) from offline_client_session cs
|
||||
@@ -524,7 +566,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c
|
||||
|
||||
폐기 목록에 실린 것도 아니다.
|
||||
|
||||
```bash label="[kc-lab-1] ⑥ 폐기 목록을 센다"
|
||||
```bash label="[lab host] ⑥ 폐기 목록을 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c \
|
||||
"select count(*) as revoked_count from revoked_token"
|
||||
```
|
||||
@@ -556,7 +598,7 @@ t3 와 t4 의 순서가 전부다. 응답을 만들던 요청은 이미 성공
|
||||
|
||||
**정책을 바꿔 두 번 더 잰다.** 한 번 더 재기 전에 세션을 새로 만든다 — 파괴된 세션으로 재면 전부 `400` 이다.
|
||||
|
||||
```bash label="[kc-lab-1] ⑦ 구성 B — 회전을 끈다"
|
||||
```bash label="[lab host] ⑦ 구성 B — 회전을 끈다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
update realms/keycloak-patterns -s revokeRefreshToken=false
|
||||
```
|
||||
@@ -578,7 +620,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
전부 `200` 이고 세션도 멀쩡하다(observed, `04-policy-comparison.txt`). 같은 refresh token 을 계속 쓸 수 있으므로 경쟁 자체가 성립하지 않는다. 대신 잃는 것이 있다 — 토큰이 유출되면 만료까지 계속 쓸 수 있고, 회전의 목적이 그 창을 좁히는 것이었다.
|
||||
|
||||
```bash label="[kc-lab-1] ⑧ 구성 C — 회전을 켜고 재사용 1회를 허용한다"
|
||||
```bash label="[lab host] ⑧ 구성 C — 회전을 켜고 재사용 1회를 허용한다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
update realms/keycloak-patterns -s revokeRefreshToken=true -s refreshTokenMaxReuse=1
|
||||
```
|
||||
@@ -624,7 +666,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
① 두 값을 한 번에 되돌리고 시각을 남긴다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 회전을 끄고 시각을 남긴다"
|
||||
```bash label="[lab host] ① 회전을 끄고 시각을 남긴다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
update realms/keycloak-patterns -s revokeRefreshToken=false -s refreshTokenMaxReuse=0
|
||||
date '+%H:%M:%S 회전 끔'
|
||||
@@ -632,7 +674,7 @@ date '+%H:%M:%S 회전 끔'
|
||||
|
||||
② 똑같은 명령으로 다시 읽는다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 되돌아갔는지 다시 읽는다"
|
||||
```bash label="[lab host] ② 되돌아갔는지 다시 읽는다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get realms/keycloak-patterns \
|
||||
--fields revokeRefreshToken,refreshTokenMaxReuse,accessTokenLifespan
|
||||
@@ -654,7 +696,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
① 파드를 직접 지운다. `--rm` 이 없으므로 자동으로 사라지지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 탐침 파드를 지운다"
|
||||
```bash label="[lab host] ① 탐침 파드를 지운다"
|
||||
kubectl -n keycloak-lab delete pod b3-probe --ignore-not-found
|
||||
```
|
||||
|
||||
@@ -666,7 +708,7 @@ https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/log
|
||||
|
||||
③ 세션 수를 센다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 세션 수를 센다"
|
||||
```bash label="[lab host] ③ 세션 수를 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- \
|
||||
psql -U keycloak -d keycloak -c 'select offline_flag, count(*) from offline_user_session group by 1'
|
||||
```
|
||||
|
||||
+45
-29
@@ -41,7 +41,9 @@ Redis 를 0대로 내렸을 때 무엇이 멈추는지 재는 절차다. 세 경
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 `kc-lab-1` 에서 `kubectl` 로 친다. `kubectl` 에 `sudo` 를 붙이지 않는다 — root 홈에는 kubeconfig 가 없어 `localhost:8080` 으로 붙으려다 끝난다. 브라우저는 시작 전에 한 번 쓴다. 세션이 Redis 에 하나는 있어야 잃는 것이 보인다.
|
||||
명령은 `[lab host]` 에서 `kubectl` 로 친다. `kubectl` 에 `sudo` 를 붙이지 않는다 — root 홈에는 kubeconfig 가 없어 `localhost:8080` 으로 붙으려다 끝난다. 브라우저는 시작 전에 한 번 쓴다. 세션이 Redis 에 하나는 있어야 잃는 것이 보인다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -81,13 +83,13 @@ A-2 에서 Keycloak 의 PostgreSQL 을 내렸을 때는 이렇게 됐다.
|
||||
|
||||
**저장소를 지우는 실험이다.** Redis 를 0대로 내리고 나중에 볼륨 없이 파드를 지운다. 그 안의 세션은 돌아오지 않고 로그인한 사용자는 전부 로그아웃된다. 중간에 그만두려면 한 줄이면 된다.
|
||||
|
||||
```bash label="[kc-lab-1] 중간에 그만둘 때 치는 한 줄"
|
||||
```bash label="[lab host] 중간에 그만둘 때 치는 한 줄"
|
||||
kubectl -n keycloak-lab scale deployment/redis --replicas=1
|
||||
```
|
||||
|
||||
볼륨을 뗀 뒤에는 매니페스트를 다시 적용해 되돌린다. 그 두 줄이 둘째 주입의 유일한 되돌리기다. `deploy/lab/k8s/bff-redis.yaml` 은 저장소 안의 상대 경로다 — 저장소를 체크아웃한 디렉터리에서 쳐야 풀리고, 다른 디렉터리에서 치면 경로가 없다는 오류로 끝나 볼륨이 안 돌아온다. 그 체크아웃이 `kc-lab-1` 의 어디에 있는지는 원본 가이드에 없다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] 볼륨을 뗀 뒤에 되돌리는 두 줄"
|
||||
```bash label="[lab host] 볼륨을 뗀 뒤에 되돌리는 두 줄"
|
||||
kubectl apply -f deploy/lab/k8s/bff-redis.yaml
|
||||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
```
|
||||
@@ -102,7 +104,7 @@ kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
|
||||
**무엇을 보는가** — 파드 넷의 상태와 배치.
|
||||
|
||||
```bash label="[kc-lab-1] 파드 배치를 본다"
|
||||
```bash label="[lab host] 파드 배치를 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
|
||||
@@ -122,7 +124,7 @@ redis-... 1/1 Running 0 3d ... kc-lab-2
|
||||
|
||||
**무엇을 보는가** — 키 수와 두 영속화 설정.
|
||||
|
||||
```bash label="[kc-lab-1] Redis 의 내용과 영속화 설정을 본다"
|
||||
```bash label="[lab host] Redis 의 내용과 영속화 설정을 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli ping
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli config get save
|
||||
@@ -154,7 +156,7 @@ kubectl -n keycloak-lab exec deploy/redis -- redis-cli config get appendonly
|
||||
|
||||
**무엇을 보는가** — 영속화를 말하기 전에 확인할 셋. 이 확인을 건너뛰면 「AOF 를 켰는데 안 남는다」를 「Redis 가 이상하다」로 읽게 된다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 파드에 볼륨이 붙어 있는가"
|
||||
```bash label="[lab host] ① 파드에 볼륨이 붙어 있는가"
|
||||
kubectl -n keycloak-lab get pod -l app=redis \
|
||||
-o jsonpath='{.items[0].spec.volumes}'; echo
|
||||
```
|
||||
@@ -165,7 +167,7 @@ kubectl -n keycloak-lab get pod -l app=redis \
|
||||
[{"name":"data","persistentVolumeClaim":{"claimName":"redis-data"}}]
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 어디에 붙었는지와 PVC 상태를 본다"
|
||||
```bash label="[lab host] ② 어디에 붙었는지와 PVC 상태를 본다"
|
||||
kubectl -n keycloak-lab get pod -l app=redis \
|
||||
-o jsonpath='{.items[0].spec.containers[0].volumeMounts}'; echo
|
||||
kubectl -n keycloak-lab get pvc
|
||||
@@ -197,7 +199,7 @@ Redis 는 이것을 모른다. `appendonly yes` 를 켜면 성실히 `/data` 에
|
||||
|
||||
**무엇을 보는가** — 주입 후에 볼 세 경로를 주입 전에 똑같은 명령으로.
|
||||
|
||||
```bash label="[kc-lab-1] 세 경로의 상태 코드를 뽑는다"
|
||||
```bash label="[lab host] 세 경로의 상태 코드를 뽑는다"
|
||||
for p in / /bff/token-boundary /actuator/health; do
|
||||
curl -s -o /dev/null -w "$p %{http_code}\n" --max-time 10 "https://app1.hyeonworks.com$p"
|
||||
done
|
||||
@@ -226,7 +228,7 @@ done
|
||||
|
||||
**무엇을 보는가** — 세 응답의 본문. `/actuator/**` 는 이 실험대에서 열려 있다(운영에서는 절대 안 연다).
|
||||
|
||||
```bash label="[kc-lab-1] ① health 그룹 셋의 본문을 받는다"
|
||||
```bash label="[lab host] ① health 그룹 셋의 본문을 받는다"
|
||||
curl -s https://app1.hyeonworks.com/actuator/health; echo
|
||||
curl -s https://app1.hyeonworks.com/actuator/health/readiness; echo
|
||||
curl -s https://app1.hyeonworks.com/actuator/health/liveness; echo
|
||||
@@ -252,11 +254,25 @@ curl -s https://app1.hyeonworks.com/actuator/health/liveness; echo
|
||||
/actuator/health/liveness liveness 그룹
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② kubelet 이 보는 경로를 확인한다"
|
||||
```bash label="[lab host] ② kubelet 이 보는 경로를 확인한다"
|
||||
kubectl -n keycloak-lab get deploy bff \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].readinessProbe.httpGet.path}'; echo
|
||||
```
|
||||
|
||||
**2026-09-17 에 Redis 를 내리기 전과 후를 같은 세 줄로 쟀다**(observed). 세 그룹이 어디서 갈리는지가 이 편의 전부다.
|
||||
|
||||
```text
|
||||
Redis 살아 있을 때 Redis 0대일 때
|
||||
/actuator/health UP DOWN ← redis: RedisConnectionFailureException
|
||||
/actuator/health/readiness UP UP ← kubelet 이 보는 경로
|
||||
/actuator/health/liveness UP UP
|
||||
|
||||
파드 1/1 Running 1/1 Running (둘 다)
|
||||
Service 엔드포인트 ready true,true ready true,true
|
||||
```
|
||||
|
||||
**Redis 가 통째로 사라졌는데 쿠버네티스는 아무것도 안 한다.** 합산 `health` 만 `DOWN` 이고 kubelet 이 보는 `readiness` 는 `UP` 이라 Service 가 두 파드로 트래픽을 계속 보낸다. `/actuator/health` 를 프로브로 걸었다면 두 파드가 동시에 빠져 전면 장애가 됐을 것이고, `readiness` 로 건 지금은 **아무 신호도 안 난다** — 어느 쪽이 맞는지가 아니라 **무엇을 고르면 무엇을 못 보게 되는지**가 이 세 줄에 있다.
|
||||
|
||||
```text
|
||||
/actuator/health/readiness
|
||||
```
|
||||
@@ -283,7 +299,7 @@ kubectl -n keycloak-lab get deploy bff \
|
||||
|
||||
① 시각을 남기고 replica 를 0으로 내린다.
|
||||
|
||||
```bash label="[kc-lab-1] 시각을 남기고 Redis 를 0대로 내린다"
|
||||
```bash label="[lab host] 시각을 남기고 Redis 를 0대로 내린다"
|
||||
date '+%H:%M:%S 정지'
|
||||
kubectl -n keycloak-lab scale deployment/redis --replicas=0
|
||||
```
|
||||
@@ -307,14 +323,14 @@ deployment.apps/redis scaled
|
||||
|
||||
**⓪ 먼저 Redis 를 다시 올린다.** 바로 앞 절에서 0대로 내려 두었고, 이 절의 명령은 전부 파드가 살아 있어야 한다. 올리지 않고 이어 치면 `exec deploy/redis` 가 붙을 파드를 못 찾는다.
|
||||
|
||||
```bash label="[kc-lab-1] ⓪ 앞 절의 주입을 되돌린다"
|
||||
```bash label="[lab host] ⓪ 앞 절의 주입을 되돌린다"
|
||||
kubectl -n keycloak-lab scale deployment/redis --replicas=1
|
||||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
```
|
||||
|
||||
① `volumeMounts` 와 `volumes` 를 함께 뗀다. 가이드가 이 방향을 미검증으로 표시했다(unknown) — 원래 실행은 반대 순서였다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 볼륨 참조를 떼고 롤아웃을 기다린다"
|
||||
```bash label="[lab host] ① 볼륨 참조를 떼고 롤아웃을 기다린다"
|
||||
kubectl -n keycloak-lab patch deployment redis --type=json \
|
||||
-p '[{"op":"remove","path":"/spec/template/spec/containers/0/volumeMounts"},
|
||||
{"op":"remove","path":"/spec/template/spec/volumes"}]'
|
||||
@@ -323,7 +339,7 @@ kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
|
||||
② AOF 를 켜고 키를 심은 뒤 `/data` 를 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② AOF 를 켜고 키를 심는다"
|
||||
```bash label="[lab host] ② AOF 를 켜고 키를 심는다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli config set appendonly yes
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli config get appendonly
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli set b5:aof "written-with-aof"
|
||||
@@ -343,7 +359,7 @@ kubectl -n keycloak-lab exec deploy/redis -- ls -la /data
|
||||
|
||||
**네 확인이 같은 시점을 보지 않는다.** ①②③ 은 첫째 주입이 걸려 있는 동안에만 성립한다 — 주입 1 절을 친 직후, 주입 2 절의 ⓪ 으로 Redis 를 다시 올리기 전에 본다. ④ 는 둘째 주입을 친 뒤라 Redis 가 1대로 살아 있을 때 본다. 절 순서대로 위에서 아래로 한 번에 치면 ① 이 `1/1` 을 내는데, 그것은 스케일이 안 먹은 증상이 아니라 ⓪ 이 제대로 올린 결과다.
|
||||
|
||||
```bash label="[kc-lab-1] ① Redis 가 0대인가"
|
||||
```bash label="[lab host] ① Redis 가 0대인가"
|
||||
kubectl -n keycloak-lab get pods -l app=redis
|
||||
kubectl -n keycloak-lab get deploy redis
|
||||
```
|
||||
@@ -361,7 +377,7 @@ redis 0/0 0 0 3d
|
||||
|
||||
**응답이 없는 것과 붙지 못하는 것은 다르다.** 로그가 이유를 말한다.
|
||||
|
||||
```bash label="[kc-lab-1] ② BFF 로그에서 연결 시도를 찾는다"
|
||||
```bash label="[lab host] ② BFF 로그에서 연결 시도를 찾는다"
|
||||
kubectl -n keycloak-lab logs -l app=bff --tail=40 | grep -iE 'redis|connect|netty' | tail -10
|
||||
```
|
||||
|
||||
@@ -378,7 +394,7 @@ kubectl -n keycloak-lab logs -l app=bff --tail=40 | grep -iE 'redis|connect|nett
|
||||
|
||||
`pollConnect` 와 `finishConnect` 는 연결을 맺는 중이라는 뜻이다. 이미 실패한 것이 아니라 아직 시도 중이고, Lettuce(Netty 기반 Redis 클라이언트)가 재연결을 시도하며 타임아웃을 기다린다. 관찰 절의 `000` 이 여기서 나온다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 엉뚱한 것을 죽이지 않았는지 본다"
|
||||
```bash label="[lab host] ③ 엉뚱한 것을 죽이지 않았는지 본다"
|
||||
kubectl -n keycloak-lab get pods
|
||||
```
|
||||
|
||||
@@ -392,7 +408,7 @@ bff-555df79c97-vgg6g 1/1 Running 0 16m
|
||||
|
||||
**둘째 주입도 걸렸는지 본다.** 볼륨 확인은 주입 전과 똑같은 명령이다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 볼륨이 정말 떨어졌는가"
|
||||
```bash label="[lab host] ④ 볼륨이 정말 떨어졌는가"
|
||||
kubectl -n keycloak-lab get pod -l app=redis \
|
||||
-o jsonpath='{.items[0].spec.volumes}'; echo
|
||||
```
|
||||
@@ -416,7 +432,7 @@ kubectl -n keycloak-lab get pod -l app=redis \
|
||||
|
||||
**`000` 은 오류가 아니라 멈춤이다.** 주입 전과 똑같은 명령을 친다.
|
||||
|
||||
```bash label="[kc-lab-1] 세 경로를 다시 친다"
|
||||
```bash label="[lab host] 세 경로를 다시 친다"
|
||||
for p in / /bff/token-boundary /actuator/health; do
|
||||
curl -s -o /dev/null -w "$p %{http_code}\n" --max-time 10 "https://app1.hyeonworks.com$p"
|
||||
done
|
||||
@@ -456,7 +472,7 @@ done
|
||||
|
||||
**그런데 파드는 `Ready` 를 유지한다.** 이 절차의 가장 중요한 발견이다.
|
||||
|
||||
```bash label="[kc-lab-1] health 그룹 셋을 코드와 본문으로 본다"
|
||||
```bash label="[lab host] health 그룹 셋을 코드와 본문으로 본다"
|
||||
curl -s -o /dev/null -w 'health %{http_code}\n' --max-time 10 https://app1.hyeonworks.com/actuator/health
|
||||
curl -s -o /dev/null -w 'readiness %{http_code}\n' --max-time 10 https://app1.hyeonworks.com/actuator/health/readiness
|
||||
curl -s -o /dev/null -w 'liveness %{http_code}\n' --max-time 10 https://app1.hyeonworks.com/actuator/health/liveness
|
||||
@@ -489,9 +505,9 @@ curl -s https://app1.hyeonworks.com/actuator/health/readiness; echo
|
||||
|
||||
그래서 Service 에서 파드를 빼지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] 엔드포인트가 아직 ready 인지 본다"
|
||||
```bash label="[lab host] 엔드포인트가 아직 ready 인지 본다"
|
||||
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=bff \
|
||||
-o custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
|
||||
-o "custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready"
|
||||
```
|
||||
|
||||
```text
|
||||
@@ -533,7 +549,7 @@ liveness 에는 넣지 않는다. liveness 가 실패하면 kubelet 이 파드
|
||||
|
||||
**첫째 주입을 되돌리고 손대지 않는다.** BFF 를 재시작하고 싶은 충동을 참는다 — 재시작하면 스스로 회복하는가를 영영 알 수 없다.
|
||||
|
||||
```bash label="[kc-lab-1] ① Redis 를 다시 올린다"
|
||||
```bash label="[lab host] ① Redis 를 다시 올린다"
|
||||
date '+%H:%M:%S 복구'
|
||||
kubectl -n keycloak-lab scale deployment/redis --replicas=1
|
||||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
@@ -547,7 +563,7 @@ deployment.apps/redis scaled
|
||||
deployment "redis" successfully rolled out
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 회복했는지와 재시작 횟수를 본다"
|
||||
```bash label="[lab host] ② 회복했는지와 재시작 횟수를 본다"
|
||||
for p in /actuator/health /bff/token-boundary; do
|
||||
curl -s -o /dev/null -w "$p %{http_code}\n" --max-time 10 "https://app1.hyeonworks.com$p"
|
||||
done
|
||||
@@ -568,7 +584,7 @@ kubectl -n keycloak-lab get pods -l app=bff
|
||||
|
||||
`rollout status` 가 돌아와도 지운 파드가 아직 종료 중일 수 있다. 이어지는 `exec deploy/redis` 가 그 파드에 붙으면 명령이 실패하거나 지우기 전 숫자를 낸다. 이 절차의 판정이 바로 그 `dbsize` 이므로, 숫자가 이상하면 `get pods -l app=redis` 로 `Running` 하나만 남았는지 보고 다시 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 볼륨 없이 파드를 지우고 남은 것을 센다"
|
||||
```bash label="[lab host] ③ 볼륨 없이 파드를 지우고 남은 것을 센다"
|
||||
kubectl -n keycloak-lab delete pod -l app=redis
|
||||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||||
@@ -597,7 +613,7 @@ deployment "redis" successfully rolled out
|
||||
|
||||
볼륨을 되돌리고 같은 시험을 다시 하면 결과가 갈린다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 볼륨을 되돌리고 같은 시험을 다시 한다"
|
||||
```bash label="[lab host] ④ 볼륨을 되돌리고 같은 시험을 다시 한다"
|
||||
kubectl apply -f deploy/lab/k8s/bff-redis.yaml
|
||||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli set b5:pvc "written-on-pvc"
|
||||
@@ -642,7 +658,7 @@ deployment "redis" successfully rolled out
|
||||
|
||||
PVC 도 노드에 못박힌다.
|
||||
|
||||
```bash label="[kc-lab-1] PVC 의 스토리지 클래스를 본다"
|
||||
```bash label="[lab host] PVC 의 스토리지 클래스를 본다"
|
||||
kubectl get pvc -n keycloak-lab redis-data -o jsonpath='{.spec.storageClassName}'; echo
|
||||
```
|
||||
|
||||
@@ -683,7 +699,7 @@ Prometheus 가 긁는 대상에 Redis·PostgreSQL·BFF 가 애초에 없다(obse
|
||||
|
||||
① 관찰 절에서 이미 쳤더라도 한 번 더 친다. `apply` 는 같은 결과를 낸다.
|
||||
|
||||
```bash label="[kc-lab-1] 매니페스트를 다시 적용한다"
|
||||
```bash label="[lab host] 매니페스트를 다시 적용한다"
|
||||
kubectl apply -f deploy/lab/k8s/bff-redis.yaml
|
||||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
```
|
||||
@@ -700,7 +716,7 @@ kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
|
||||
① 접두어로만 지운다. **`FLUSHALL` 은 치지 않는다** — BFF 세션과 oauth2-proxy 세션이 같은 Redis 에 있다.
|
||||
|
||||
```bash label="[kc-lab-1] 실험 키만 지우고 남은 키를 본다"
|
||||
```bash label="[lab host] 실험 키만 지우고 남은 키를 본다"
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli del b5:aof b5:pvc b5:probe
|
||||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan
|
||||
```
|
||||
|
||||
+85
-33
@@ -37,7 +37,9 @@ realm 에 RSA 서명 키를 하나 더해 겹치는 구간을 만들고, 옛 공
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 전부 `[kc-lab-1]` 에서 친다. Keycloak 이미지에는 `curl` 도 `wget` 도 없어서(`exit 127`) 파드 안에서 HTTP 요청을 보낼 수 없다. JWKS(JSON Web Key Set, 서버가 공개키를 싣는 목록)와 토큰은 호스트에서 공개 이름으로 치고, `kcadm.sh` 만 `kubectl exec` 로 감싸 파드 안에서 돌린다.
|
||||
명령은 전부 `[lab host]` 에서 친다. Keycloak 이미지에는 `curl` 도 `wget` 도 없어서(`exit 127`) 파드 안에서 HTTP 요청을 보낼 수 없다. JWKS(JSON Web Key Set, 서버가 공개키를 싣는 목록)와 토큰은 호스트에서 공개 이름으로 치고, `kcadm.sh` 만 `kubectl exec` 로 감싸 파드 안에서 돌린다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
터미널은 하나면 된다. 붙잡아 두어야 하는 셸이 없고, 대신 `OLD` 과 `NEW` 두 변수를 끝까지 들고 가므로 중간에 터미널을 닫지 않는다.
|
||||
|
||||
@@ -76,14 +78,23 @@ realm 에 RSA 서명 키를 하나 더해 겹치는 구간을 만들고, 옛 공
|
||||
- `05-keycloak` 이 끝나 있고 realm `keycloak-patterns` 에 클라이언트 `bff-confidential` 과 사용자 `labuser` 가 있다.
|
||||
- B-0 이 끝나 BFF 가 떠 있다.
|
||||
- 리소스 서버(`echo`, 네임스페이스 `header-lab`)가 떠 있다. 이 절차의 401 과 200 은 전부 그 앱이 판정한다.
|
||||
- **`echo` 가 발행자에 닿아야 한다.** 그 앱은 `SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI` 와 `…JWK_SET_URI` 를 **`https://auth.hyeonworks.com/…`** 로 들고 있어서, 인증서 단계를 안 끝낸 실험대에서는 JWKS 를 못 받아 **모든 토큰을 `401` 로 떨어뜨린다.** 2026-09-17 에 그 상태에서 재 보니 이랬다(observed).
|
||||
|
||||
```text
|
||||
echo 가 발행자에 닿나 issuer=000 (curl exit 7)
|
||||
유효한 토큰으로 /api/echo 401
|
||||
토큰 없이 /api/echo 200
|
||||
```
|
||||
|
||||
`401` 과 `200` 이 뒤집혀 보이지만 키 회전과는 무관하다 — 검증기가 공개키를 못 구해 전부 거절했고, 보호되지 않은 경로만 통과했다. **이 편의 판정은 TLS 가 서 있어야 성립한다.**
|
||||
|
||||
**이건 되돌릴 수 없는 실험이다.** 지우는 것은 서명 키 공급자이고 그 안의 개인키가 함께 사라진다. 같은 이름으로 공급자를 다시 만들어도 새 키 쌍이 생기고 `kid` 가 달라지므로, 옛 키로 서명된 토큰은 영구히 검증되지 않는다. 실험대에서만 한다.
|
||||
|
||||
되돌릴 수 있는 것은 주입 하나다. 방금 만든 공급자를 지우면 원래대로 돌아간다. id 는 주입이 화면에 찍어 주는 값이고 그 줄을 그대로 옮겨 친다 — 원래 실행에서는 `7902af43-a0cc-4ebd-ad25-04d563854d16` 이었다. 아래 블록에 박힌 값이 그 원래 실행의 id 라, 8 절을 친 뒤에 그 출력이 찍어 준 자기 id 로 바꿔야 지워진다. 8 절을 치기 전에는 지울 공급자가 없다.
|
||||
되돌릴 수 있는 것은 주입 하나다. 방금 만든 공급자를 지우면 원래대로 돌아간다. id 는 주입이 화면에 찍어 주는 값이다 — 원래 실행에서는 `7902af43-a0cc-4ebd-ad25-04d563854d16` 이었다. 아래 블록의 `{{NEW_PROVIDER_ID}}` 를 8 절 출력이 찍어 준 자기 id 로 바꿔야 지워진다. 8 절을 치기 전에는 지울 공급자가 없다.
|
||||
|
||||
```bash label="[kc-lab-1] 관찰 절로 넘어가기 전에 그만둘 때 — id 를 8 절 출력의 자기 값으로 바꾼다"
|
||||
```bash label="[lab host] 관찰 절로 넘어가기 전에 그만둘 때 — id 를 8 절 출력의 자기 값으로 바꾼다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
delete components/7902af43-a0cc-4ebd-ad25-04d563854d16 -r keycloak-patterns
|
||||
delete components/{{NEW_PROVIDER_ID}} -r keycloak-patterns
|
||||
```
|
||||
|
||||
## 주입 전에 같은 명령으로 먼저 본다
|
||||
@@ -100,14 +111,14 @@ kcadm 로그인 → 키 공급자 목록 → JWKS 원문 → 토큰의 kid →
|
||||
|
||||
**행동** — 관리자 자격증명으로 로그인하고, 값이 넘어갔는지는 길이로만 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① kcadm 에 로그인한다"
|
||||
```bash label="[lab host] ① kcadm 에 로그인한다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
config credentials --server http://localhost:8080 --realm master --user admin \
|
||||
--password "$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)"
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-1] ② 비밀번호의 길이만 센다"
|
||||
```bash label="[lab host] ② 비밀번호의 길이만 센다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
|
||||
```
|
||||
@@ -122,27 +133,45 @@ kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
|
||||
**무엇을 보는가** — 이 realm 에 어떤 키 공급자가 있고 각각의 id 가 무엇인지.
|
||||
|
||||
```bash label="[kc-lab-1] 공급자 목록을 필드 셋으로 받는다"
|
||||
```bash label="[lab host] 공급자 목록을 필드 셋으로 받는다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get components -r keycloak-patterns --fields id,name,providerId
|
||||
```
|
||||
|
||||
**어디를 보나** — JSON 배열이 여러 줄로 나온다. `providerId` 가 `rsa-generated` 인 항목이 서명 키 공급자이고 `hmac-generated` · `aes-generated` 등이 함께 나온다. `"name" : "rsa-generated"` 인 항목의 `"id"` 를 지금 적어 둔다. 관찰 절에서 지울 대상이다.
|
||||
|
||||
**이 값이 뜻하는 것** — 여기서 「키 공급자만 걸러 보자」는 시도가 빈 결과를 준다. 가이드가 이 줄을 미검증으로 표시했다(unknown) — 원래 실행에서 이렇게 치고 아무것도 못 받았다.
|
||||
**이 값이 뜻하는 것** — 가이드는 「키 공급자만 걸러 보자」는 아래 시도가 빈 결과를 준다고 적고 미검증으로 표시했다.
|
||||
|
||||
```bash label="[kc-lab-1] 이렇게 치면 조용히 빈 결과다 (unknown)"
|
||||
```bash label="[lab host] 키 공급자만 걸러 본다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get components -r keycloak-patterns -q type=org.keycloak.keys.KeyProvider
|
||||
```
|
||||
|
||||
오류도 종료코드도 없이 비어 있다. 「키 공급자가 하나도 없구나」로 읽으면 이 절차 전체가 무너진다. 빈 출력은 「없다」가 아니라 「이 명령으로는 안 보인다」일 수 있고, `--fields` 로 전체를 받아 눈으로 고른다.
|
||||
**2026-09-17 에 쳐 보니 결과가 나왔다**(observed). 26.7.0 에서 이 질의는 공급자 넷을 그대로 돌려준다.
|
||||
|
||||
```text
|
||||
"name":"rsa-enc-generated" "name":"hmac-generated-hs512" "name":"aes-generated" "name":"rsa-generated"
|
||||
```
|
||||
|
||||
**빈 결과를 봤다면 세션이 없어서일 수 있다.** 같은 날 세션이 끊긴 상태로 먼저 쳤을 때는 이렇게 끝났다(observed).
|
||||
|
||||
```text
|
||||
No server specified. Use --server, or 'kcadm.sh config credentials'.
|
||||
```
|
||||
|
||||
어느 쪽이든 판정은 같다 — **빈 출력은 「없다」가 아니라 「이 명령으로는 안 보인다」일 수 있다.** 그때는 `--fields` 로 전체를 받아 눈으로 고른다.
|
||||
|
||||
:::warning
|
||||
|
||||
**`kcadm` 세션은 파드 안에 산다.** `/opt/keycloak/.keycloak/kcadm.config` 에 놓이므로 `rollout restart` 나 이미지 교체로 파드가 갈리면 **그 파일째 사라진다.** 그러면 `401` 이 아니라 `No server specified` 로 끝나고, 앞 절에서 `config credentials` 를 이미 쳤어도 소용없다. 파드를 갈아 끼운 뒤에는 다시 친다(2026-09-17, observed).
|
||||
|
||||
:::
|
||||
|
||||
### 3. JWKS 원문을 한 번 통째로 본다
|
||||
|
||||
**무엇을 보는가** — 어떤 필드가 실려 있는지. 다음부터 무엇으로 걸를지가 여기서 정해진다.
|
||||
|
||||
```bash label="[kc-lab-1] ① JWKS 를 자르지 않고 본다"
|
||||
```bash label="[lab host] ① JWKS 를 자르지 않고 본다"
|
||||
curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs
|
||||
```
|
||||
|
||||
@@ -154,7 +183,7 @@ curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-con
|
||||
|
||||
그 뒤로 `kty` · `alg` · `use` · `n` · `e` 가 이어지고 다음 키가 온다. `kid` 마다 `alg` 가 따로 붙는다. 읽을 만하게 자를 때는 `jq` 가 없으므로 `tr` 로 쉼표를 줄바꿈으로 바꾼다.
|
||||
|
||||
```bash label="[kc-lab-1] ② kid 만 뽑아 본다"
|
||||
```bash label="[lab host] ② kid 만 뽑아 본다"
|
||||
curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs \
|
||||
| tr ',' '\n' | grep kid
|
||||
```
|
||||
@@ -181,14 +210,14 @@ curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-con
|
||||
|
||||
JWKS 는 키 하나가 `}` 로 끝나므로 `tr '}'` 로 자르면 한 줄이 한 키가 된다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 키 단위로 잘라 RS256 만 센다 (unknown)"
|
||||
```bash label="[lab host] ① 키 단위로 잘라 RS256 만 센다 (unknown)"
|
||||
curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs \
|
||||
| tr '}' '\n' | grep -c RS256
|
||||
```
|
||||
|
||||
Keycloak 자신에게 묻는 쪽이 확실하고 그쪽이 1순위 도구다.
|
||||
|
||||
```bash label="[kc-lab-1] ② Keycloak 에 직접 묻는다 (unknown)"
|
||||
```bash label="[lab host] ② Keycloak 에 직접 묻는다 (unknown)"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get keys -r keycloak-patterns
|
||||
```
|
||||
@@ -203,7 +232,7 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
**행동** — 토큰 엔드포인트와 클라이언트 비밀을 변수에 담고 direct grant 로 받는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 옛 키로 서명된 토큰을 받고 길이만 본다"
|
||||
```bash label="[lab host] ① 옛 키로 서명된 토큰을 받고 길이만 본다"
|
||||
KC=https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/token
|
||||
CS=$(kubectl -n keycloak-lab get secret bff-secrets \
|
||||
-o jsonpath='{.data.KEYCLOAK_CLIENT_SECRET}' | base64 -d)
|
||||
@@ -228,14 +257,29 @@ echo "${#OLD}자"
|
||||
|
||||
**무엇을 보는가** — JWT 의 첫 토막이 헤더이고 거기 `kid` 가 있다.
|
||||
|
||||
```bash label="[kc-lab-1] 토큰 헤더를 디코드한다"
|
||||
```bash label="[lab host] 토큰 헤더를 디코드한다"
|
||||
echo "$OLD" | cut -d. -f1 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
|
||||
```
|
||||
|
||||
**어디를 보나** — 원래 실행의 모양은 이렇다(observed).
|
||||
|
||||
```json
|
||||
{"alg":"RS256","typ":"JWT","kid":"OY-caYDNGoP4HMAz-Q9UPTU-DM1i896NuzUZu6gfCqM"}
|
||||
{"alg":"RS256","typ" : "JWT","kid" : "OY-caYDNGoP4HMAz-Q9UPTU-DM1i896NuzUZu6gfCqM"}
|
||||
```
|
||||
|
||||
**콜론 양옆의 공백을 눈여겨본다.** Keycloak 은 토큰 **헤더**를 `"typ" : "JWT"` 처럼 공백을 넣어 찍고, 페이로드는 `"sid":"…"` 처럼 붙여 찍는다. 그래서 페이로드에서 되던 `sed` 가 헤더에서는 빈손으로 돌아온다. 2026-09-17 에 같은 토큰 하나로 두 형태를 나란히 쳤다(observed).
|
||||
|
||||
```text
|
||||
헤더: {"alg":"RS256","typ" : "JWT","kid" : "HKy0uQhg-vlQackK6-oj3hW6vKbDj-95Wlvdgl37cGg"}
|
||||
'"kid":"' 로 뽑으면 : []
|
||||
'"kid" *: *"' 로 뽑으면: [HKy0uQhg-vlQackK6-oj3hW6vKbDj-95Wlvdgl37cGg]
|
||||
```
|
||||
|
||||
헤더에서 값을 뽑을 때는 공백을 허용한다.
|
||||
|
||||
```bash label="[lab host] 헤더에서 kid 만 뽑는다 — 콜론 양옆 공백을 허용한다"
|
||||
echo "$OLD" | cut -d. -f1 | tr '_-' '/+' | base64 -d 2>/dev/null \
|
||||
| sed -n 's/.*"kid" *: *"\([^"]*\)".*/\1/p'
|
||||
```
|
||||
|
||||
증거 파일에는 이렇게 남아 있다(observed, `01-before-rotation.txt`).
|
||||
@@ -250,13 +294,13 @@ echo "$OLD" | cut -d. -f1 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
|
||||
|
||||
**무엇을 보는가** — 대조군. 이 확인을 건너뛰면 뒤의 401 이 아무 의미가 없다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 상태줄과 본문을 함께 본다"
|
||||
```bash label="[lab host] ① 상태줄과 본문을 함께 본다"
|
||||
curl -s -i -H "Authorization: Bearer $OLD" https://app1.hyeonworks.com/api/me
|
||||
```
|
||||
|
||||
200 이면 `subject` 같은 클레임이 돌아오고, 401 이면 `WWW-Authenticate` 헤더에 이유가 붙는다. 이 헤더를 한 번 봐 두면 뒤에서 401 이 났을 때 왜인지 물을 근거가 생긴다. 여러 번 비교할 때부터는 코드만 뽑는다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 상태 코드만 뽑는다"
|
||||
```bash label="[lab host] ② 상태 코드만 뽑는다"
|
||||
curl -s -o /dev/null -w 'old %{http_code}\n' \
|
||||
-H "Authorization: Bearer $OLD" https://app1.hyeonworks.com/api/me
|
||||
```
|
||||
@@ -286,7 +330,7 @@ Keycloak 의 키 회전은 바꾸기가 아니라 더 높은 우선순위로 추
|
||||
|
||||
**행동** — 공급자를 만들고 시각을 남긴다.
|
||||
|
||||
```bash label="[kc-lab-1] priority 200 짜리 RSA 공급자를 만든다"
|
||||
```bash label="[lab host] priority 200 짜리 RSA 공급자를 만든다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
create components -r keycloak-patterns \
|
||||
-s name=rsa-rotated -s providerId=rsa-generated \
|
||||
@@ -314,7 +358,7 @@ Created new component with id '7902af43-a0cc-4ebd-ad25-04d563854d16'
|
||||
|
||||
### 9. JWKS 에 옛 키가 남아 있는가
|
||||
|
||||
```bash label="[kc-lab-1] 3 절과 똑같은 줄을 다시 친다"
|
||||
```bash label="[lab host] 3 절과 똑같은 줄을 다시 친다"
|
||||
curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs \
|
||||
| tr ',' '\n' | grep kid
|
||||
```
|
||||
@@ -336,7 +380,7 @@ curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-con
|
||||
|
||||
`OLD` 은 건드리지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] 새 토큰을 받고 헤더를 읽는다"
|
||||
```bash label="[lab host] 새 토큰을 받고 헤더를 읽는다"
|
||||
NEW=$(curl -s -X POST "$KC" \
|
||||
-d grant_type=password -d client_id=bff-confidential -d "client_secret=$CS" \
|
||||
-d username=labuser -d password=labpass -d scope=openid \
|
||||
@@ -355,7 +399,7 @@ echo "$NEW" | cut -d. -f1 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
|
||||
|
||||
### 11. 둘 다 통해야 겹치는 구간이 무중단이다
|
||||
|
||||
```bash label="[kc-lab-1] 두 토큰을 같은 두 줄로 친다"
|
||||
```bash label="[lab host] 두 토큰을 같은 두 줄로 친다"
|
||||
curl -s -o /dev/null -w 'old %{http_code}\n' \
|
||||
-H "Authorization: Bearer $OLD" https://app1.hyeonworks.com/api/me
|
||||
curl -s -o /dev/null -w 'new %{http_code}\n' \
|
||||
@@ -374,7 +418,7 @@ curl -s -o /dev/null -w 'new %{http_code}\n' \
|
||||
|
||||
여기서 `old` 가 401 이면 둘 중 하나다. 토큰이 만료됐거나(60초), 추가 말고 다른 것을 건드렸다. 가르는 법은 옛 토큰의 `exp` 를 보는 것이고 JWT 의 가운데 토막이 클레임이다.
|
||||
|
||||
```bash label="[kc-lab-1] 만료인지 아닌지 가른다"
|
||||
```bash label="[lab host] 만료인지 아닌지 가른다"
|
||||
echo "$OLD" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
|
||||
date +%s
|
||||
```
|
||||
@@ -391,14 +435,14 @@ date +%s
|
||||
|
||||
**무엇을 보는가** — 남길 것과 지울 것의 id. `-q` 는 여전히 안 먹는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 목록을 다시 받는다"
|
||||
```bash label="[lab host] ① 목록을 다시 받는다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get components -r keycloak-patterns --fields id,name,providerId
|
||||
```
|
||||
|
||||
목록이 길면 그 항목 둘레만 잘라 본다. `"id"` 는 `"name"` 보다 위에 나온다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 지울 항목 둘레만 본다"
|
||||
```bash label="[lab host] ② 지울 항목 둘레만 본다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get components -r keycloak-patterns --fields id,name,providerId \
|
||||
| grep -B2 '"name" : "rsa-generated"'
|
||||
@@ -412,10 +456,10 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
|
||||
**목적** — 옛 서명 키를 JWKS 에서 없앤다.
|
||||
|
||||
**행동** — 위 출력의 id 를 변수에 옮기고 지운다. 아래 블록은 그대로 붙여넣으면 안 된다. 첫 줄의 `980ee9b7-...` 은 원래 실행의 값이므로 12 절 출력에서 읽은 자기 id 로 바꾼다. 안 바꾸고 치면 없는 컴포넌트를 지우라는 요청이 되어 옛 공급자는 살아 있고, 15 절이 `옛 200` 을 내 결론이 뒤집힌다.
|
||||
**행동** — 위 출력의 id 를 변수에 옮기고 지운다. 첫 줄의 `{{OLD_PROVIDER_ID}}` 를 12 절 출력에서 읽은 자기 id 로 바꾼 뒤에 친다. **그대로 붙여넣으면 셸이 멈춘다** — `{{ }}` 는 셸 문법이 아니라서 눈앞에서 실패한다. 전에는 첫 줄에 원래 실행의 값이 그대로 박혀 있었는데, 그것은 **유효한 대입이라 조용히 돌았다**: `OLDID` 에 그 문자열이 들어가고 아래 `delete` 가 실제로 나가 없는 컴포넌트를 지우라는 요청이 되고, 옛 공급자는 살아 있고, 15 절이 `옛 200` 을 내 결론이 뒤집힌다.
|
||||
|
||||
```bash label="[kc-lab-1] 옛 공급자를 지우고 시각을 남긴다"
|
||||
OLDID=980ee9b7-... # ← 위 출력에서 그대로 옮긴다. 환경마다 다르다
|
||||
```bash label="[lab host] 옛 공급자를 지우고 시각을 남긴다"
|
||||
OLDID={{OLD_PROVIDER_ID}} # ← 12 절 출력의 id 를 그대로 옮긴다. 환경마다 다르다
|
||||
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
delete components/"$OLDID" -r keycloak-patterns
|
||||
@@ -435,7 +479,7 @@ date '+%H:%M:%S 제거'
|
||||
|
||||
### 14. JWKS 에서 사라졌는지 본다
|
||||
|
||||
```bash label="[kc-lab-1] 또 같은 줄을 친다"
|
||||
```bash label="[lab host] 또 같은 줄을 친다"
|
||||
curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs \
|
||||
| tr ',' '\n' | grep kid
|
||||
```
|
||||
@@ -455,7 +499,7 @@ curl -s https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-con
|
||||
|
||||
**치기 전에 `$OLD` 가 아직 안 죽었는지 본다.** 11 절의 만료 확인 두 줄을 그대로 다시 친다. `exp` 가 `date +%s` 보다 작으면 아래에서 나올 `old 401` 은 키 제거가 아니라 만료이고, 두 401 은 화면에서 똑같이 보인다.
|
||||
|
||||
```bash label="[kc-lab-1] 11 절과 똑같은 두 줄"
|
||||
```bash label="[lab host] 11 절과 똑같은 두 줄"
|
||||
curl -s -o /dev/null -w 'old %{http_code}\n' \
|
||||
-H "Authorization: Bearer $OLD" https://app1.hyeonworks.com/api/me
|
||||
curl -s -o /dev/null -w 'new %{http_code}\n' \
|
||||
@@ -478,7 +522,7 @@ curl -s -o /dev/null -w 'new %{http_code}\n' \
|
||||
|
||||
**목적** — 401 이 캐시 상태 때문인지 가른다.
|
||||
|
||||
```bash label="[kc-lab-1] echo 를 다시 띄우고 기다린다"
|
||||
```bash label="[lab host] echo 를 다시 띄우고 기다린다"
|
||||
kubectl -n header-lab rollout restart deploy/echo
|
||||
kubectl -n header-lab rollout status deploy/echo --timeout=180s
|
||||
```
|
||||
@@ -522,7 +566,7 @@ deployment "echo" successfully rolled out
|
||||
|
||||
수명 세 값은 realm 설정이므로 직접 볼 수 있다. 가이드가 이 줄을 미검증으로 표시했다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] realm 의 수명 세 값을 받는다 (unknown)"
|
||||
```bash label="[lab host] realm 의 수명 세 값을 받는다 (unknown)"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
get realms/keycloak-patterns --fields accessTokenLifespan,ssoSessionIdleTimeout,ssoSessionMaxLifespan
|
||||
```
|
||||
@@ -575,6 +619,14 @@ kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
|
||||
| 「캐시 때문일 것」이라 재시작을 기다린다 | 캐시는 유예를 주지 않는다 | 재시작 전후가 같다 |
|
||||
| 지운 키를 되살리려 한다 | 되살릴 수 없다. 같은 이름과 같은 키는 다르다 | 새 키 하나만 남은 상태가 정상이다 |
|
||||
|
||||
## 이 실험대에서 아직 못 밟은 단계
|
||||
|
||||
2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.**
|
||||
|
||||
- **남은 것** — 키 공급자 추가·삭제와 `echo` 가 내는 `401`/`200` 판정 전부. `echo` 가 JWKS 를 `https://auth.hyeonworks.com` 에서 받으므로 인증서가 서야 한다. 지금까지 밟은 것 — realm·사용자·`echo` 배포, JWKS 와 `kid` 읽기, 공급자 목록.
|
||||
- **막는 것** — `https://auth.hyeonworks.com` 이 서지 않는다. 와일드카드 인증서(Cloudflare API 토큰이 필요한 DNS-01)와, 밖에서 실험대에 닿는 길(호스트의 libvirt `guest_input` 구멍 — A-4 에서 확인한 `ExecStartPost` 누락)이 둘 다 있어야 한다.
|
||||
- **그때까지 이 편의 실측 가운데 `(observed)` 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다.** 나머지는 원래 실행의 값이다.
|
||||
|
||||
## 무엇이 관측이고 무엇이 아닌가
|
||||
|
||||
이 절차의 숫자는 `2026-09-04 14:30–14:32 KST` 에 돈 한 번의 실행에서 나왔다(observed). 해설 문서 머리의 `15:50–16:00 KST` 는 문서를 쓴 시각이고 증거 파일의 mtime 이 앞의 값이라, 실측으로 인용하는 것은 뒤쪽이라고 가이드가 적는다.
|
||||
|
||||
Reference in New Issue
Block a user