Files
keycloak-pattern/docs/experiment-plan.md
T
DongHyeonkaandClaude Opus 5 f1e8c35805 docs: plan all remaining experiments with architecture, injection points and predictions
Twenty experiments across four layers, each with a topology diagram marking where the fault goes in, the metrics to watch, a falsifiable prediction written before the run, and a pass/fail rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 10:50:23 +09:00

34 KiB

실험 실행 계획 — 구조 · 주입 지점 · 확인 항목

session-store-lab-roadmap.md무엇을 할지의 지도라면, 이 문서는 각 실험을 어떤 구조에서 어디를 건드려 무엇을 볼지를 미리 확정해둔 것이다.

실험 전에 예측을 적어둔다. 결과를 보고 나서 "그럴 줄 알았다"고 쓰면 아무것도 배우지 못한다. 예측이 빗나갔을 때가 배울 거리다.

맥락이 안 잡히면 session-lab-prerequisites.md 먼저.


1. 기준 구조 — 모든 실험이 여기서 출발한다

                        [개발 노트북]  브라우저 · kubectl · Playwright
                              │
                              │  https  (tailnet 100.x, split DNS)
                              ▼
   ┌──────────────────────────────────────────────────────────────┐
   │  test-server   Arch Linux · 12GB RAM · WiFi only              │
   │                                                               │
   │    nginx :443   ← TLS 종료 (Let's Encrypt, DNS-01)            │
   │       auth.hyeonworks.com / app1 / app2                       │
   └───────────────────────────┬───────────────────────────────────┘
                               │  http, libvirt NAT (virbr0)
               ┌───────────────┴────────────────┐
               ▼                                ▼
   ┌───────────────────────┐        ┌───────────────────────┐
   │  kc-lab-1   5120MB    │        │  kc-lab-2   4096MB    │
   │  k3s server           │        │  k3s agent            │
   │  10.42.0.0/24         │        │  10.42.1.0/24         │
   │                       │        │                       │
   │   traefik   ← 진입점  │        │                       │
   │   coredns             │        │                       │
   │   metrics-server      │        │                       │
   │   local-path-prov.    │        │                       │
   │                       │◀ 7800 ▶│                       │
   │   keycloak-1          │ JGroups│   keycloak-0          │
   │                       │        │   postgres  (PVC)     │
   │   prometheus (PVC)    │        │                       │
   │   grafana             │        │                       │
   └───────────────────────┘        └───────────────────────┘
        ▲                                    ▲
        │ 관측 스택은 여기 고정               │ 장애 주입은 여기
        │ (nodeSelector 로 못박음)            │

배치 규칙 — 왜 이렇게 두었는가

규칙 이유
관측 스택은 kc-lab-1 고정 관측 시스템이 관측 대상과 같이 죽으면 기록이 없다
장애 주입은 kc-lab-2 server 를 죽이면 노드 상실이 아니라 컨트롤 플레인 상실이다
postgres 는 kc-lab-2 DB 손실과 노드 손실을 한 실험에서 겹쳐 볼 수 있다
PVC 는 노드에 못박힘 local-path 프로비저너. 결함이 아니라 조건

공통 관측 지점

이후 모든 실험에서 이 표의 것들을 본다.

무엇 어디서 값의 뜻
up Prometheus 0이라는 값으로 남는다. 죽은 시각을 사후에 특정할 수 있는 유일한 지표
vendor_statistics_approximate_entries_unique{cache="sessions"} KC :9000 노드별 세션 캐시 보유량
vendor_jgroups_fd_sock2_get_num_suspected_members KC :9000 상대를 의심하기 시작한 순간
vendor_jgroups_merge3_get_views KC :9000 split brain 후 재병합
ISPN000094 kubectl logs 새 클러스터 뷰(멤버 명단)
JGROUPS_PING 테이블 PostgreSQL 디스커버리 등록 상태
offline_user_session (offline_flag='0') PostgreSQL 온라인 세션의 진실
node_load1, node_memory_* node-exporter 기계가 죽었나 프로세스가 죽었나

2. 전체 목록

# 실험 답하는 실무 질문 상태 선행
A-0 세션 복제 확인 정상일 때 세션은 어디 있나
A-1 TCP 7800 차단 노드 간 통신이 끊기면? 다음 A-0
A-2 DB 정상 정지 DB가 죽으면? A-0
A-3 DB 강제 종료 복구 시 무엇을 잃나 (RPO) A-2
A-4 노드 전원 차단 Keycloak 한 대가 죽으면? A-0
A-5 비대칭 파티션 한쪽만 안 들리면? (split brain) A-1
A-6 지연 주입 (netem) 느려지기만 하면? A-1
A-7 volatile 모드 비교 옛 방식(KC≤24)은 뭐가 다른가 A-1·A-2·A-4
A-8 롤링 재시작 배포하면 로그아웃되나 A-0
B-1 BFF + Redis 저장소 Redis vs JDBC 어느 쪽인가 → Q3 배포 필요
B-2 BFF 다중 인스턴스 인스턴스 늘리면? → Q1 B-1
B-3 Refresh 토큰 경쟁 동시 갱신하면? → Q2 B-1
B-4 Edge 인가 범위 인가를 Edge에 얼마나? → Q4 B-1
B-5 Redis 상실 Redis가 죽으면? B-1
B-6 Redis 영속화 비교 AOF/RDB 유무 차이 B-5
C-1 다중 앱 SSO SSO 붙이면 뭐가 달라지나 B-1
C-2 백채널 로그아웃 로그아웃이 전 앱에 퍼지나 C-1
D-1 백업·복구 리허설 복구해본 적 있나 A-2
D-2 버전 업그레이드 올릴 때 세션·스키마는? D-1
D-3 비밀 관리 Secret은 정말 감춰지나
D-4 인증서 갱신 갱신 중 무중단인가

A층 — Keycloak 자체

A-1. TCP 7800 차단

답하려는 질문

노드 사이 네트워크가 끊기면 로그인한 사용자는 어떻게 되는가?

이 실험이 A-0 다음인 이유 — 통념과 측정이 정면으로 어긋나 있다. 둘 중 하나는 틀렸고, 이 실험이 판정한다.

통념 (KC 24 이전 자료) 세션은 7800으로 복제된다 → 막으면 세션 공유가 깨진다
A-0 측정 세션은 DB로 공유된다 → 막아도 안 깨진다

구조 — 주입 지점

   kc-lab-1                              kc-lab-2
   ┌────────────────┐                    ┌────────────────┐
   │  keycloak-1    │ ✂✂✂ 7800 차단 ✂✂✂ │  keycloak-0    │
   │                │   (NetworkPolicy)  │                │
   └───────┬────────┘                    └───────┬────────┘
           │                                     │
           │         둘 다 DB 는 그대로 본다      │
           └──────────────┬──────────────────────┘
                          ▼
                  ┌───────────────┐
                  │  postgres     │  ← 여기는 안 건드린다
                  │  JGROUPS_PING │     디스커버리는 계속 된다
                  │  세션 테이블   │
                  └───────────────┘

핵심: 디스커버리(DB)는 살리고 트랜스포트(7800)만 끊는다. 그래서 "DB엔 서로 등록되는데 클러스터는 안 붙는" 상태가 만들어진다. 단일 노드에서는 재현 불가능한 고장이며, 이 실험대를 2노드로 만든 이유다.

주입 방법

# NetworkPolicy 로 파드 간 7800 만 막는다. IP 가 아니라 라벨로 지정한다
# (파드 IP 는 재시작마다 바뀐다 — 2홉 실험에서 확인)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: block-jgroups, namespace: keycloak-lab }
spec:
  podSelector: { matchLabels: { app: keycloak } }
  policyTypes: [Ingress]
  ingress:
    - from: [{ podSelector: { matchLabels: { app: keycloak } } }]
      ports: [{ port: 7800, protocol: TCP }]   # ← 이 규칙을 뺀 채 적용하면 차단

확인할 것

관측 예상
fd_sock2_get_num_suspected_members 0 → 1 (가장 먼저 움직일 것)
ISPN000094 로그 각자 멤버 1개짜리 뷰 발행 (split brain)
JGROUPS_PING 테이블 양쪽 다 남아 있다 (디스커버리는 살아 있으므로)
교차 노드 refresh 200 유지 ← 이게 판정의 핵심
로그아웃 전파 ? — DB 삭제로 되면 유지, 클러스터 메시지면 깨진다
sessions 캐시 엔트리 변화 없음

예측

세션 공유    안 깨진다     (A-0 근거: 세션은 7800 으로 안 다닌다)
로그아웃     안 깨진다     (DB 행 삭제로 전파되므로)
realm 설정 변경 전파   깨진다  (work 캐시는 클러스터 메시지를 쓴다)
로그인 실패 횟수 집계  깨진다  (loginFailures 캐시)

판정 기준

결과
교차 refresh 가 200 A-0 모델이 맞다. 통념이 틀렸다
교차 refresh 가 400/401 A-0 해석이 틀렸다. 무엇을 놓쳤는지 다시 판다

복구

kubectl -n keycloak-lab delete networkpolicy block-jgroups
# MERGE3 가 뷰를 다시 합치는지 확인 — merge3_get_views 증가

A-2. DB 정상 정지

답하려는 질문

PostgreSQL이 죽으면 어떻게 되는가? 로그인한 사람은? 새 로그인은?

구조

   kc-lab-1                              kc-lab-2
   ┌────────────────┐                    ┌────────────────┐
   │  keycloak-1    │◀────── 7800 ──────▶│  keycloak-0    │
   │  캐시: 세션 N개 │    (살아 있다)      │  캐시: 세션 M개 │
   └───────┬────────┘                    └───────┬────────┘
           │                                     │
           └──────────────┬──────────────────────┘
                          ▼
                  ┌───────────────┐
                  │  postgres  ✗  │  ← replicas=0 으로 정지
                  └───────────────┘

   질문: 캐시에 세션을 들고 있는 노드는 그 세션을 계속 쓸 수 있는가?

A-0에서 얻은 예측의 시험대다. DB가 진실의 원천이라면, 캐시에 있어도 못 쓴다는 것이 논리적 귀결이다. 하지만 룩어사이드 캐시라면 읽기는 될 수도 있다 — 이게 갈리는 지점이다.

주입 방법

kubectl -n keycloak-lab scale deployment/postgres --replicas=0

확인할 것 — 세 가지 경로를 구분해서 본다

경로 무엇을 보는가 예상
① 캐시 있는 노드에서 refresh 캐시가 DB를 대신할 수 있나 실패 (쓰기가 필요)
② 캐시 없는 노드에서 refresh 완전히 DB 의존 실패
③ 새 로그인 쓰기 경로 실패
④ 이미 발급된 access token 검증 서명만 보면 되는 경로 성공 (60초 동안)

④가 중요하다. DB가 죽어도 access token 수명 동안은 리소스 서버가 정상 동작한다. 이게 "짧은 access token + 긴 refresh token" 구조가 장애에 주는 유예 시간이다.

관측 예상
up{job="keycloak"} 1 유지 — 프로세스는 살아 있다
KC /health/ready DOWN 으로 바뀌는가?
KC 로그 커넥션 풀 고갈 / FATAL: the database system is shutting down
에러 응답 코드 500? 503? — 클라이언트가 재시도할지 결정하는 값

예측

로그인          실패
refresh         실패 (캐시 유무 무관)
기존 access token 검증   성공 — 만료까지
KC 프로세스     살아 있음 (up=1) — 헬스체크만 DOWN

판정 기준

up=1인데 기능은 전멸 → "프로세스 살아 있음"이 헬스체크로 부족하다는 증명. /health/ready가 DB를 보는지 여부가 롤링 배포 안전성을 좌우한다.

복구

kubectl -n keycloak-lab scale deployment/postgres --replicas=1
# 자동 복구되는가, Keycloak 재시작이 필요한가 ← 이것도 측정 대상

A-3. DB 강제 종료 — 무엇을 잃는가 (RPO)

답하려는 질문

정상 종료가 아니라 전원이 나가면 무엇을 잃는가?

A-0에서 발견한 SET LOCAL synchronous_commit TO OFF의 시험대다.

update OFFLINE_USER_SESSION set LAST_SESSION_REFRESH=$1, VERSION=$2 where ...
SET LOCAL synchronous_commit TO OFF      WAL 플러시를 기다리지 않고 커밋
COMMIT

구조

   ┌────────────────┐
   │  keycloak-0    │  refresh 를 초당 여러 번 날린다
   └───────┬────────┘
           │  UPDATE LAST_SESSION_REFRESH
           ▼
   ┌───────────────────────────────┐
   │  postgres                     │
   │    메모리 (WAL 버퍼)  ← 여기까지만 쓰고 COMMIT 응답  │
   │        ↓ 아직 안 내려감        │
   │    디스크 (WAL)                │  ✗ SIGKILL
   └───────────────────────────────┘
        이 사이의 갱신이 사라진다

주입 방법

# 정상 종료가 아니라 강제 종료. 컨테이너에 SIGKILL
kubectl -n keycloak-lab delete pod -l app=postgres --grace-period=0 --force

확인할 것

절차 무엇을 보는가
1. refresh 를 반복하며 매번 LAST_SESSION_REFRESH 를 기록 기준값
2. 마지막 성공 시각을 클라이언트 쪽에 남긴다 클라이언트가 믿는 값
3. 강제 종료 → 재기동
4. DB의 LAST_SESSION_REFRESH 를 다시 읽는다 2와 4의 차이 = 잃은 것
5. postgres 로그의 crash recovery 메시지 database system was not properly shut down

예측

잃는 양      수백 밀리초 분량의 LAST_SESSION_REFRESH 갱신
잃지 않는 것  로그인·로그아웃 자체 (다른 트랜잭션일 가능성 — 확인 필요)
crash recovery  자동 실행되고 성공

"COMMIT 응답을 받았는데 없어졌다"를 실제로 관측하는 것이 목표다. 이건 버그가 아니라 의도된 트레이드오프이며, 그걸 눈으로 봐야 "세션 갱신은 잃어도 되는 쓰기"라는 설계 판단을 체감할 수 있다.


A-4. 노드 전원 차단

답하려는 질문

Keycloak 한 대가 통째로 죽으면 그 노드가 처리하던 사용자는 어떻게 되는가?

구조

   kc-lab-1  (살아 있다)                 kc-lab-2  ✗ 전원 차단
   ┌────────────────────┐                ┌────────────────────┐
   │  traefik  ← 진입점  │                │  keycloak-0        │
   │  coredns           │                │    캐시 세션 M개 ✗  │
   │  keycloak-1        │                │  postgres      ✗    │  ← 같이 죽는다
   │  prometheus        │                │                    │
   └────────────────────┘                └────────────────────┘

   ⚠ postgres 가 kc-lab-2 에 있으므로 이 실험은 A-2 와 겹친다.
      순수한 "노드 상실"을 보려면 postgres 를 먼저 kc-lab-1 로 옮긴다.

중요virsh destroy는 프로세스 kill이 아니라 전원 차단이다. 게스트에 종료 신호가 가지 않으므로 진짜 노드 상실이다.

두 판본으로 나눠 실행한다

판본 배치 보는 것
4a. 순수 노드 상실 postgres 를 kc-lab-1 로 옮긴 뒤 Keycloak 한 대만 잃었을 때
4b. 복합 장애 현재 배치 그대로 노드 + DB 동시 상실 — 더 현실적

주입 방법

virsh destroy kc-lab-2        # ACPI 종료가 아니라 전원 차단

확인할 것

관측 예상 (4a)
up{node="kc-lab-2"} 0 — 언제 죽었는지 기록됨
죽은 노드의 마지막 샘플 node-exporter 가 DaemonSet 인 이유
kubectl 노드 상태 ReadyNotReady (기본 40초 후)
죽은 노드가 만든 세션의 refresh 성공 ← 판정 핵심. DB 에 있으므로
ISPN000094 남은 노드가 멤버 1개 뷰 발행
JGROUPS_PING 죽은 노드 행이 남아 있다 (지울 주체가 없다)
파드 재배치 PVC 때문에 postgres 는 재배치 못 한다

예측

4a  세션 살아남는다. 남은 노드가 DB 에서 읽어 계속 서비스한다
4b  전면 장애 — DB 가 같이 죽었으므로 A-2 와 같아진다

복구

virsh start kc-lab-2
# JGROUPS_PING 의 유령 행이 어떻게 정리되는지 관찰 — 자동인가 수동인가

A-5. 비대칭 파티션 (split brain)

답하려는 질문

한쪽 방향만 끊기면? 서로 상대가 죽었다고 생각하면?

가장 고약한 장애다. 완전 단절보다 부분 단절이 훨씬 흔하고 진단이 어렵다.

구조

   kc-lab-1                              kc-lab-2
   ┌────────────────┐   ──────────▶      ┌────────────────┐
   │  keycloak-1    │   나가는 건 됨      │  keycloak-0    │
   │                │                    │                │
   │                │   ◀━━✗━━━━━━       │                │
   │                │   들어오는 건 막힘  │                │
   └────────────────┘                    └────────────────┘

   kc-lab-1 → "상대가 응답이 없네, 죽었나보다"
   kc-lab-2 → "나는 잘 보내고 있는데?"

주입 방법

# 게스트 안에서 nftables 로 인바운드만 차단 (커널이 분리돼 있어 가능)
ssh kc-lab-2 'sudo nft add rule inet filter input tcp dport 7800 drop'

확인할 것

관측 보는 이유
양쪽의 ISPN000094 서로 다른 명단을 갖는가
coord 컬럼 양쪽 다 코디네이터가 되는가 (진짜 split brain)
fd_sock2_suspected 어느 쪽이 먼저 의심하는가
세션 동작 DB 는 멀쩡하므로 영향 없어야 한다
복구 후 merge3_get_views MERGE3 가 합치는가, 재시작이 필요한가

A-6. 지연 주입 (tc netem)

답하려는 질문

끊기지 않고 느려지기만 하면? — 실제 장애의 대부분이 이 모양이다

구조

   keycloak ──▶ [ tc netem: +200ms ] ──▶ postgres
                     지연만 넣는다

주입 방법

ssh kc-lab-2 'sudo tc qdisc add dev eth0 root netem delay 200ms'

확인할 것

관측
refresh 응답 시간 분포 200ms 가 그대로 더해지는가, 증폭되는가
DB 커넥션 풀 사용률 느려지면 커넥션이 반환 안 돼 고갈된다
타임아웃 발생 지점 어느 계층이 먼저 포기하는가
낙관적 락 충돌 증가 트랜잭션이 길어져 VERSION 충돌이 늘어야 한다

여기서 A-0의 낙관적 락 발견이 다시 쓰인다 — 지연이 길수록 읽고 쓰는 사이가 벌어지므로 충돌 확률이 오른다.


A-7. volatile 모드 비교 — 옛 방식은 뭐가 다른가

답하려는 질문

Keycloak 24 이전 방식(메모리가 진실의 원천)이었다면 지금까지의 결론이 어떻게 달라지는가?

A-0의 모든 결론이 "26 기본값"이라는 전제 위에 있다. 전제를 뒤집어 확인하는 것이 이 실험이다.

구조

   현재 (persistent, KC 26 기본)          비교군 (volatile)
   ┌────────────────┐                    ┌────────────────┐
   │  keycloak      │                    │  keycloak      │
   │   캐시(로컬)    │                    │   캐시  ←──────┼── 진실의 원천
   └───────┬────────┘                    └───────┬────────┘
           │ 진실의 원천                          │ ✗ 세션 저장 안 함
           ▼                                     ▼
      [ PostgreSQL ]                        [ PostgreSQL ]
                                            (realm 설정만)

설정 변경

args: ["start", "--features-disabled=persistent-user-sessions"]

확인할 것 — A-0부터 A-4를 전부 다시 돌린다

실험 persistent (측정 완료) volatile (예측)
A-0 캐시 위치 각자 자기 것만 양쪽에 복제되어야 한다
A-1 7800 차단 안 깨짐 치명적 — 정반대
A-2 DB 정지 전면 장애 세션은 살아남음
A-4 노드 상실 세션 생존 복제됐다면 생존
A-8 롤링 재시작 세션 유지 전부 로그아웃

A-1이 정반대로 뒤집히는 것을 보는 게 이 실험의 목적이다. 같은 주입, 같은 관측, 정반대 결과 — 그래야 "버전 확인이 1순위"라는 교훈이 말이 아니라 데이터가 된다.


A-8. 롤링 재시작 — 배포하면 로그아웃되는가

답하려는 질문

운영에서 가장 자주 겪는 것. 배포할 때마다 사용자가 로그아웃되는가?

구조

   시각 →
   keycloak-0  ████████──✗ 재시작 ──████████
   keycloak-1  ████████████████──✗ 재시작 ──████

   사용자 세션  ────────────────────────────────▶  유지되는가?

주입 방법

kubectl -n keycloak-lab rollout restart statefulset/keycloak

확인할 것

관측 예상 (persistent)
재시작 전후 sid 동일
재시작 중 refresh 일시 실패 후 성공 (파드 전환 시점)
재시작 후 캐시 엔트리 0에서 다시 시작 — 캐시는 사라진다
브라우저 SSO 쿠키 유지

A-7과 짝이다. volatile 이었다면 여기서 전원 로그아웃된다. 이게 persistent 를 켜는 진짜 이유다.


B층 — 애플리케이션 세션 (Redis)

선행: 배포가 필요하다. 현재 실험대에 BFF도 Redis도 없다.

   지금                          B층 실험에 필요한 것
   ┌──────────────┐             ┌──────────────────────────┐
   │ keycloak x2  │             │ keycloak x2              │
   │ postgres     │             │ postgres                 │
   │ echo (헤더용) │      +      │ bff x2       ← 새로 배포  │
   │              │             │ redis        ← 새로 배포  │
   │              │             │ resource-server          │
   └──────────────┘             └──────────────────────────┘

BFF 구현은 develop-keycloak-pattern3에 있으므로 가져온다.

git checkout develop-keycloak-pattern3 -- bff/

자원 예산 확인 필요 — 호스트 12GB 중 현재 여유 약 4GB. Redis(~64Mi) + BFF 2개(~512Mi) 정도면 들어가지만, A층 실험을 마친 뒤 배포하는 편이 안전하다.

B-1. BFF 저장소 결정 → Q3

구조

   브라우저 ──쿠키(SESSION)──▶ ┌─────────┐          ┌──────────────┐
                              │ bff-0   │─────────▶│              │
                              ├─────────┤          │    Redis     │
                              │ bff-1   │─────────▶│              │
                              └─────────┘          └──────────────┘
                                   │                 두 가지가 들어간다
                                   │                 ① Application Session
                                   │                 ② OAuth2AuthorizedClient
                                   ▼
                              [ Keycloak ]

확인할 것 — 조회 키가 다르다는 것이 핵심

상태 조회 키 결과
Application Session session ID 브라우저마다 별개
OAuth2AuthorizedClient registration + principal name 같은 사용자의 여러 브라우저가 공유

5단계 — 로그인 유지·재시작 복구 / refresh token 이 평문으로 저장되는가 / session TTL ≠ token 만료 / logout 후 잔여 항목 / 저장소 끊김 시 오류 형태

B-2. 다중 인스턴스 운영 → Q1

구조

   nginx  ip_hash 주석 → 스티키 유무를 같은 구성에서 비교
      │
      ├──▶ bff-0 ──┐
      └──▶ bff-1 ──┴──▶ Redis

확인 핵심authorized client 덮어쓰기. "Redis만 붙이면 해결"이라는 착각을 깨는 항목이다. 조회 키에 session ID가 없어서 생긴다.

B-3. Refresh Token 경쟁 → Q2

구조

   access token 만료 직후, 동시에 두 요청
        │                    │
        ▼                    ▼
     bff-0                bff-1
        │                    │
        └────── 같은 refresh token ──────▶ Keycloak
                                             │
                            한쪽만 성공, 다른 쪽은?

★ B-1 이후여야 재현된다. 저장소가 process-local이면 두 replica가 같은 항목을 보지 않아 경쟁 자체가 성립하지 않는다.

A-0에서 낙관적 락(VERSION)과 SKIP LOCKED를 이미 확인했다. Keycloak 쪽 동작을 알고 시작하므로, BFF 쪽 재시도 설계에 집중한다.

판정 기준 — 실패가 사용자에게 노출되면 lock, 노출 안 되면 재시도.

B-4. Edge 인가 범위 → Q4

구조

   브라우저 ──▶ nginx ──▶ [auth_request] ──▶ BFF
                  │                           │
                  │◀── X-Auth-Request-Roles ──┘
                  │
                  └──▶ Resource Server   ← 헤더만 믿는가?

2홉 실험의 교훈이 여기서 결정적이다 — 헤더 위조가 통하면 그것은 쿠키 속성이 아니라 신원 위조다. NetworkPolicy 패턴을 재사용한다.

확인 — 다중 값 구분자·escaping / 헤더 크기 상한 / role 변경 반영 시점 / upstream 이 값과 service identity 까지 보는가

B-5 · B-6. Redis 상실과 영속화

   B-5   Redis 파드 정지        → 세션 전멸? 로그인 화면?
   B-6   appendonly yes/no 비교 → 재시작 후 무엇이 남는가

A-2·A-3의 Redis 판본이다. 같은 질문을 다른 저장소에 던진다.


C층 — SSO

C-1. 다중 앱 SSO

답하려는 질문

당신 원래 질문: "SSO를 추가하면 어떻게 달라지는가"

구조

                    ┌──────────────────────────┐
                    │   Keycloak SSO 세션       │
                    │   쿠키 KEYCLOAK_IDENTITY  │
                    └────────┬─────────────────┘
                             │
              ┌──────────────┴──────────────┐
              ▼                             ▼
      ┌──────────────┐             ┌──────────────┐
      │  app1        │             │  app2        │
      │ (BFF 세션)    │             │ (BFF 세션)    │
      └──────────────┘             └──────────────┘

   app1 로그인 → app2 는 로그인 화면 없이 통과
   Keycloak 세션 소멸 → 양쪽 다 재로그인

확인할 것

항목 보는 것
app1 로그인 후 app2 접속 로그인 화면 없이 통과하는가
A-2(DB 정지)를 겹친다 장애 반경이 앱 하나에서 전체로 커지는가
SSO 세션 vs 앱 세션 수명 어긋나면 무슨 일이 생기는가
노드 상실 중 SSO 새 앱 진입이 되는가

이게 SSO의 대가다 — 편의를 얻는 대신 단일 장애점이 하나 늘어난다.

C-2. 백채널 로그아웃

   Keycloak ──logout token(sid)──▶ app1 /backchannel-logout
            └─────────────────────▶ app2 /backchannel-logout

A-0에서 확인한 sid가 여기서 쓰인다. 토큰·DB·관리 API에서 같았던 그 문자열이 로그아웃 전파의 키다.

확인 — 한 앱이 응답 안 하면? 부분 실패 시 재시도는? 역인덱스는 어디에?


D층 — 운영 역량

D-1. 백업과 복구 리허설

"백업이 있다"와 "복구해봤다"는 완전히 다르다.

   1. pg_dump 로 전체 덤프
   2. 일부러 파괴 (PVC 삭제 또는 DROP DATABASE)
   3. 덤프에서 복구
   4. 로그인이 되는가 / 기존 세션이 살아나는가
   5. 걸린 시간 = RTO,  잃은 데이터 = RPO

A-3에서 측정한 RPO와 합쳐진다. synchronous_commit OFF 때문에 백업 시점 이후뿐 아니라 크래시 직전 수백 ms도 잃는다.

D-2. 버전 업그레이드

   26.7.0 → 다음 마이너
      │
      ├── DB 스키마 자동 마이그레이션  ← 실패하면 되돌리기 어렵다
      ├── 롤링 중 세션 유지되는가       ← A-8 과 연결
      └── 롤백 가능한가                ← 스키마가 바뀐 뒤에는 구버전이 못 뜬다

D-1이 선행이어야 한다. 백업 없이 스키마 마이그레이션을 돌리지 않는다.

D-3. 비밀 관리

kubectl get secret -o yaml → base64 디코드가 한 줄로 되는 것을 직접 본다. "Secret은 암호화가 아니다"를 문장이 아니라 화면으로.

D-4. 인증서 갱신

sudo certbot renew --force-renewal   # 90일을 안 기다린다

nginx reload 중 진행 중이던 요청은 어떻게 되는가.


실행 순서와 의존 관계

   A-0 ✅
    │
    ├─▶ A-1 (7800)  ─────┬─▶ A-5 (비대칭)
    │                    └─▶ A-6 (지연)
    │
    ├─▶ A-2 (DB 정지) ──▶ A-3 (강제 종료) ──▶ D-1 (백업/복구) ──▶ D-2 (업그레이드)
    │
    ├─▶ A-4 (노드 상실)
    │
    ├─▶ A-8 (롤링 재시작)
    │
    └─▶ A-7 (volatile 비교)   ← A-1·A-2·A-4·A-8 을 다 한 뒤 전부 재실행
         │
         ▼
        [ BFF + Redis 배포 ]
         │
         ├─▶ B-1 (저장소) ──┬─▶ B-2 (다중 인스턴스)
         │                  ├─▶ B-3 (refresh 경쟁)
         │                  ├─▶ B-4 (edge 인가)
         │                  └─▶ B-5 (Redis 상실) ─▶ B-6 (영속화)
         │
         └─▶ C-1 (SSO) ─▶ C-2 (백채널 로그아웃)

   D-3 (비밀), D-4 (인증서) 는 언제든 독립적으로

A-7(volatile)을 A층 마지막에 두는 이유 — 앞의 실험들을 전부 마친 뒤 설정 하나만 바꿔 재실행하면, 같은 주입에 대한 정반대 결과를 한 벌로 얻는다. 이게 이 실험대에서 가장 값진 산출물이 될 가능성이 높다.


공통 판정 규칙

A-0에서 비싸게 배운 것들. 모든 실험에 적용한다.

규칙
대조군을 먼저 잰다 반대편 응답은 발급 노드 응답과 나란히 놓기 전엔 의미 없다
개수가 아니라 식별자로 추적 실험 도구 자신이 만든 잡음에 오염된다
저장소를 실제로 건드리는 탐침 userinfo 는 세션을 몰라도 통과한다
계측 코드가 자기 실패를 말하게 스냅샷이 비면 델타가 조용히 0이 된다
예측을 먼저 적는다 사후에 "그럴 줄 알았다"는 아무것도 아니다
증거는 원본 그대로 터미널 출력과 브라우저 스크린샷. 재구성하지 않는다

실험마다 남기는 것

docs/experiment-NN-<이름>.md          해설 · 예측 · 결과 · 개념 · 함정
docs/evidence/<이름>/README.md        파일별 무엇을 보여주는지
docs/evidence/<이름>/*.txt            터미널 원문
docs/evidence/<이름>/*.png            브라우저 스크린샷 (Grafana / 관리 콘솔)
deploy/lab/scripts/experiment-*.sh    재현 스크립트