Files
keycloak-pattern/docs/experiment-followup-untested-items.md
T
DongHyeonkaandClaude Opus 5 74c9b3cea7 docs: replace prose placeholders in reproduction steps with executable commands
The audit found ~80 placeholders, and the damaging ones were where the
measuring apparatus itself was prose rather than a command:

  a6  "( curl ... ) & 를 20개 띄우고 wait"  — the 22.2s headline came from this
  a3  "<로그인 반복, sid 를 /tmp/sids 에>"  — the whole RPO measurement
  a3  "<sid 목록>"                          — the control it is compared against
  a5  "<수신 파드IP>"                       — the injection
  a8  writes /tmp/tok, reads /tmp/rt        — self-inconsistent, sent an empty token
  b3  $KC / $RT / $NEW never assigned
  c2  bare kcadm.sh with no kubectl exec
  a1  conntrack tuples written by hand, though the direction flips per restart

Each is now a shell-expandable form: pod IPs from jsonpath, the admin password
from the secret, ids from kcadm --format csv, conntrack tuples derived from
"conntrack -L" with awk rather than transcribed.

Then the rewritten commands were executed against the live cluster, and one
of them failed — the 20-way load generator, written as "kubectl run --rm -i",
lost its output stream twice in a row. That is a trap this series already hit
once, and the rewrite reintroduced it. A-6 now uses a resident probe pod that
collects into a file and is cat-ed once; verified 20/20 lines.

Evidence: docs/evidence/followup/05-command-reproducibility.txt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:01:54 +09:00

11 KiB
Raw Blame History

후속 — 미측정으로 남겼던 항목을 채운다

브랜치 feature/keycloak-followup-untested-items · 증거 docs/evidence/followup/ · 2026-09-04 17:3518:20 KST

23개 실험을 마치며 세 항목을 "못 했다" 로 남겼다. 그중 둘을 채우고, 셋째(D-4 강제 갱신)는 권한이 필요해 별도로 진행한다.


0. 결론부터

항목 결과
D-2 정방향 업그레이드 무중단. 87회 요청 전부 200
그리고 D-2 의 결론이 정밀해졌다 "롤백 불가" 는 조건부다 — 스키마가 바뀌었을 때만
B-4 ③ role 변경 반영 시점 요청 횟수와 무관하다. 세션이 새로 만들어져야 한다
B층에 Grafana 증거가 없는 이유 관측 대상에 없다. 안 찍은 것이 아니다

1. D-2 정방향 업그레이드 — 26.7.0 → 26.7.3

개념 — 왜 이 방향을 못 했었나

D-2 를 처음 할 때 26.7.0 보다 새 이미지를 몰라 역방향(26.0)만 시험했다. 태그 목록을 조회하니 26.7.1 / 26.7.2 / 26.7.3 이 있었다.

curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyActiveTags=true"

절차 — D-1 의 교훈대로 백업이 먼저다

kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
  --clean --if-exists > /tmp/pre-2673.sql          # 396333 bytes

# 1초 간격으로 외부 진입점을 찍으면서 태그를 바꾼다
( 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)"
    sleep 1
  done > /tmp/avail.txt ) &
kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.7.3
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=600s

결과 — 무중단

200 200 200 ... (87회)
  200 응답: 87 회
  비200   : 0 회
소요: 15:22:59 → 15:24:26 (87초)

업그레이드 구간의 cluster_size 와 up

파드가 하나씩 교체되며 cluster_size 가 2 → 1 → 2 를 두 번 반복한다. 각 파드의 up 시계열이 끝나고 새 시계열이 시작되는 것이 함께 보인다.

  마이그레이션: 210 → 210        ← 스키마 변경 없음
  세션:         3 → 3           ← 유지
  Infinispan:   16.0.12 → 16.0.14
  restarts=0

2. ★ 그래서 D-2 의 결론을 정밀화한다

"스키마 변경이 없었다면 롤백이 될 것" 이라는 가설이 생겼고, 시험했다.

kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.7.0
  200 응답: 43 회 / 비200: 1
  Keycloak 26.7.0
  마이그레이션: 210 · 세션: 3
  restarts=0

롤백이 성공했다.

버전 차 databasechangelog 롤백
26.7.0 → 26.0 체크섬 불일치 불가 (ValidationFailedException)
26.7.0 ↔ 26.7.3 210 → 210, 변화 없음 가능

처음 D-2 에서 "롤백은 안 된다" 고 쓴 것은 과했다. 정확히는 "스키마가 바뀌었으면 안 된다" 이고, 패치 릴리스처럼 스키마가 그대로면 태그를 되돌리는 것으로 충분하다.

판단 기준은 버전 번호가 아니라 databasechangelog 의 행 수가 바뀌었는가다.

# 업그레이드 전후로 이것만 비교하면 롤백 가능 여부를 안다
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
  "select count(*) from databasechangelog"

전환 순간의 000 1회

200 ×24  000  200 ×19

서버 오류가 아니라 --max-time 3 타임아웃이다. 파드 전환 순간 요청 하나가 3초를 넘겼다. 정방향에서는 0회였다.

"무중단" 은 관측 해상도에 달려 있다. 1초 간격·3초 타임아웃으로는 44회 중 1회가 걸렸다. 더 촘촘히 보면 더 보일 것이다.


3. B-4 ③ — role 변경은 언제 반영되는가

왜 못 했었나

B-4 를 할 때 oauth2-proxy 가 아직 배포되지 않아 "proxy session" 이 존재하지 않았다. B-7 에서 배포했으므로 이제 측정할 수 있다.

방법

X-Auth-Request-Roles 대신 이미 전달되고 있는 x-forwarded-email 을 썼다. role 을 헤더로 내보내려면 추가 설정이 필요한데, "IdP 의 클레임 변경이 언제 반영되는가" 라는 질문은 어느 클레임이든 같다.

1 · 3 단계는 브라우저 콘솔에서 실행한다. oauth2-proxy 쿠키가 HttpOnly 라 curl 로는 로그인 상태를 재현할 수 없기 때문이다(그래서 셸이 아니라 JS 다).

// 1. 기준선 — 로그인된 app2 탭의 콘솔에서
for (let i = 0; i < 3; i++) {
  const r = await (await fetch('/api/echo')).text();
  console.log(new Date().toISOString(), r.match(/x-forwarded-email: (.*)/)[1]);
}

// 3. 반복 요청 — 0.5초 간격으로 12번
for (let i = 0; i < 12; i++) {
  const r = await (await fetch('/api/echo')).text();
  console.log(new Date().toISOString(), r.match(/x-forwarded-email: (.*)/)[1]);
  await new Promise(s => setTimeout(s, 500));
}

2 단계는 셸에서:

# 2. IdP 에서 바꾼다
UID=$(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)
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
  update users/$UID -r keycloak-patterns -s email=CHANGED-labuser@example.com

# 4. Redis 세션을 지워 재인증을 강제한다 (반영 조건 확인)
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

결과 — 반영되지 않는다

2026-09-04T07:51:56.300Z  req#1   HTTP 200  x-forwarded-email=labuser@example.com
2026-09-04T07:51:56.864Z  req#2   HTTP 200  x-forwarded-email=labuser@example.com
   ...
2026-09-04T07:52:02.719Z  req#12  HTTP 200  x-forwarded-email=labuser@example.com

  → 12회 · 6.4초 동안 전부 옛 값

이 결과는 처음에 증거 파일로 남기지 않았다. 브라우저 콘솔에서 보고 문서에만 옮겨 적었고, 서브에이전트 감사가 "증거 없이 서 있는 결론" 으로 지적했다. 다시 측정해 03-b4-role-propagation.txt 에 타임스탬프와 함께 남겼다.

재측정 중 두 시계가 107초 어긋나 있다는 것도 드러났다 — 브라우저(개발 머신)와 test-server 의 시각을 그대로 비교하면 안 되고, 증거 파일에 보정값을 적어두었다.

세션을 지우고 재인증시키자

2026-09-04T07:53:01.121Z  req#1  HTTP 200  x-forwarded-email=changed-labuser@example.com
2026-09-04T07:53:01.456Z  req#2  HTTP 200  x-forwarded-email=changed-labuser@example.com
2026-09-04T07:53:01.785Z  req#3  HTTP 200  x-forwarded-email=changed-labuser@example.com

개념 — 세션은 로그인 시점의 스냅샷이다

   로그인      →  IdP 가 준 클레임을 세션에 담는다
   이후 요청    →  세션에서 읽어 헤더로 내보낸다
                   └─ IdP 를 다시 부르지 않는다
   IdP 에서 변경 →  세션은 모른다

oauth2-proxy 에 --cookie-refresh 가 설정되어 있지 않다. 설정하면 그 주기마다 토큰을 갱신하며 클레임을 다시 받는다.

설정 반영 시점
지금 (--cookie-refresh 없음) 쿠키 만료(1시간) 또는 재인증까지 안 됨
--cookie-refresh=5m 최대 5분

Q4 는 "몇 번째 요청부터 반영되는지" 를 물었는데, 답은 "요청으로는 안 된다" 이다. 요청 횟수가 아니라 세션의 나이가 정한다.

이것이 Q4 의 설계 판단 2번(role·tenant 변경이 즉시 반영돼야 하는가)에 직접 답한다 — 즉시가 필요하면 헤더 방식은 맞지 않는다.


4. B층에 Grafana 증거가 없는 이유

문서 감사에서 B-1·B-3·B-4·B-5 에 스크린샷이 없는 것이 드러나 소급해서 찍으려다 원인을 확인했다.

=== Prometheus 가 실제로 긁는 대상 ===
  keycloak         2개
  kubelet          2개
  node-exporter    2개
  prometheus       1개

=== B층 구성 요소의 지표가 있는가 ===
  redis_up                         시계열 0개
  redis_connected_clients          시계열 0개
  pg_up                            시계열 0개
  pg_stat_database_numbackends     시계열 0개

Redis 도 PostgreSQL 도 BFF 도 긁는 대상에 없다.

스크린샷을 안 찍은 것이 아니라 지표가 없다. A층이 Grafana 증거를 남길 수 있었던 것은 Keycloak 이 /metrics 를 내놓고 그것을 scrape 대상에 넣어뒀기 때문이다.

관측은 "나중에 붙이는 것" 이 아니라 실험 설계에 포함되어야 한다. A-2 에서 kube-state-metrics 가 없다는 것을, A-6 에서 응답 시간 히스토그램이 없다는 것을 찾았는데, B층 전체가 빠져 있던 것은 문서 감사를 하고서야 드러났다.

보완하려면

대상 방법
Redis redis_exporter 사이드카 또는 Deployment
PostgreSQL postgres_exporter
BFF 이미 actuator 가 있다 — /actuator/prometheus 노출 + scrape 추가
파드 readiness kube-state-metrics (A-2 에서 이미 찾은 항목)

5. 남은 것 — D-4 강제 갱신

$ sudo -n -l
sudo: a password is required

호스트 sudo 가 비밀번호를 요구해 certbot renew --force-renewalsystemctl reload nginx 를 실행할 수 없다. 사람이 함께 있어야 한다.

측정 계획은 준비되어 있다.

# 측정 쪽 (내가 실행)
while true; do
  printf '%s ' "$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 https://auth.hyeonworks.com/realms/master)"
  sleep 0.2
done

# 주입 쪽 (사람이 실행)
sudo certbot renew --force-renewal

0.2초 간격으로 재는 이유 — 2절에서 1초 간격으로는 전환을 거의 못 잡았다. nginx reload 는 그보다 훨씬 짧을 것이므로 해상도를 올려야 한다.


증거 파일

증거 수집 시각: 2026-09-04 15:23 16:17 KST (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)

파일 종류
01-d2-forward-upgrade.txt 터미널 원문
02-d2-rollback-same-schema.txt 터미널 원문
03-b4-role-propagation.txt 터미널 원문
04-observability-gap.txt 터미널 원문

파일별 상세는 evidence/followup/README.md.