Files
keycloak-pattern/docs/guides/experiments/b0-bff-redis-deploy.md
T
DongHyeonkaandClaude Opus 5 6f6ab86345 docs(guides): reproduction guides for all 26 experiments
Written by subagents running under the writing-practitioner-guides skill,
one guide per experiment, 22,566 lines. Each walks a reader from baseline
capture through injection, injection verification, observation and recovery.

Section 3 carries the weight in most of them. Injection failed silently nine
times in this lab, and a failed injection looks exactly like no effect — so
the guides verify the target is actually in the intended state before
reading any result. A-4 makes virsh list the only proof because the node
reads Ready for 40 seconds after the machine is off; A-5 makes the packet
counter the sole go/no-go because a rule on the wrong node produces an empty
result that reads like a finding; A-6 quotes the run where 적용완료 was
printed between four Cannot find device "eth0" lines.

The traps the guides are built around are ones that invert a conclusion
rather than merely annoy:

  A-0   emptying the session table without a restart leaves cache entries
        that get counted as replication arriving
  A-2   dropping -o /dev/null fuses body and status into one string
  A-3   presence of "ready to accept connections" instead of its timestamp
  B-2   row count alone reads an UPDATE as nothing having happened
  B-4   tr ',' '\n' splits ["admin","editor"] so only admin is seen
  B-7   no login screen means the cookie died and SSO re-authenticated
  C-1   counting sessions without joining realm counts your own kcadm one
  D-1   kubectl exec without -i restores nothing and still exits 0
  D-4a  "ran with error output" is what success looks like

Every quoted block is copied from docs/evidence/ and marked 실측; reshaped
commands are marked 미검증 rather than passed off as measured. Where a source
document carries a ★ correction the guides follow the corrected claim — A-7's
REVOKED_TOKEN hypothesis, C-1's session count, B-2's schema attribution.

Two hazards are stated rather than smoothed over: B-6 deletes a key that
cannot be recreated, and D-1/D-4 need host sudo, which asks for a password,
so those steps say a person must type them.

Audit over all 26: 672 interpretation pairs, 486 evidence citations, 117
undo sections, and zero occurrences of the patterns the skill forbids —
no python data processing, no deprecated kubectl get endpoints, no
placeholders, no bare kcadm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 18:29:00 +09:00

33 KiB
Raw Blame History

B-0 재현 가이드 — 아무것도 주지 않았을 때 Spring 이 무엇을 고르는지 본다

해설 문서: docs/experiment-b0-bff-redis-deploy.md · 증거 원문: docs/evidence/b0-bff-redis-deploy/

이 가이드가 끝나면

당신 터미널과 브라우저에서 이것들을 직접 본다.

보게 되는 것 어디서
돌고 있는 인스턴스가 실제로 고른 구현체 이름 /actuator/beans
Redis 도 Spring Session 도 하나도 구성되지 않은 것 같은 곳
조회 키에 session ID 가 없다는 것 빈 이름 하나가 그대로 설명이다
replica 2 에서 로그인 자체가 실패하는 것 브라우저 · /login?error
replica 를 1 로 줄이면 되는 것 같은 브라우저
브라우저에 토큰이 0개인 것 /bff/token-boundary

전제

  • 05-keycloak 이 끝나 있다. A층 실험은 안 해도 된다.
  • 브라우저가 필요하다. 인가 코드 흐름은 왕복이 두 번이라 curl 로 대신할 수 없다. https://app1.hyeonworks.com/ 이 당신 브라우저에서 열려야 한다.
  • BFF 이미지는 워크스테이션에서 빌드해서 두 노드에 밀어 넣는다. 레지스트리가 없으므로 imagePullPolicy: Never 다.
  • 명령은 kc-lab-1 에서 친다. kubectlsudo 로 쓴다.
  • jq 는 이 실험대에 깔려 있지 않다. 이 가이드는 grep 으로 읽는다.

주의 — ★ 저장소를 먼저 붙이면 이 실험은 성립하지 않는다

B-0 의 질문은 **「아무것도 주지 않았을 때 자동구성이 무엇을 고르는가」**다. Redis 를 먼저 연결하면 잴 것이 없어진다. Redis 는 배포만 하고 BFF 에 연결하지 않는다. 연결은 B-1 에서 한다.

그리고 이 저장소의 현재 소스는 이미 B-1·B-2 를 거친 뒤 상태다. bff-redis.yaml 에는 SPRING_SESSION_STORE_TYPE=redis 가 있고, SecurityConfig 에는 JdbcOAuth2AuthorizedClientService 빈이 있다. 그대로 배포하면 B-2 의 결과를 재게 된다. 어느 브랜치에도 B-0 시점의 파일은 남아 있지 않다 — 2-1 에서 손으로 되돌린다.

전 구간 약 40~60분(빌드 시간 포함). 배포한 것을 지우는 명령은 5절에 있다.

표시 규약

표시
실측 2026-09-04 13:3913:46 KST 수집 기록의 출력 원문. 증거 파일에 그대로 있다
형태 값이 매번 달라지는 출력. 모양만 보이고 숫자는 당신 것과 다르다
미검증 손으로 치기 좋게 이 가이드에서 고친 형태. 원래 실행은 다른 방법을 썼다

파드 이름·IP·빈 개수는 당신 환경에서 다르다. 이 문서는 자리표시자(<...>)를 쓰지 않는 대신, 그 값을 뽑는 명령을 먼저 적는다.


0. 왜 이 실험을 하는가

Q1 이 직접 요구한 확인이다.

코드에 저장소를 직접 생성하는 Bean 이 없기 때문에, 어떤 구현체가 실제로 사용되는지는 Spring Boot 의 자동구성 결과까지 확인해야 정확하게 알 수 있다.

   빈을 직접 만들지 않으면
        └─ Spring Boot 가 조건에 따라 고른다
             └─ 무엇을 골랐는지는 코드 어디에도 안 적혀 있다
                  └─ 돌아가는 인스턴스에 물어봐야 안다

추측으로도 답은 나온다. 「저장소를 안 붙였으니 메모리겠지.」 맞다. 그런데 추측으로 두면 안 되는 이유가 두 번째 줄에 있다.

빈 이름 하나가 이 층 전체의 문제를 담고 있는데, 그 이름은 추측으로 안 나온다. 찍어 봐야 나온다. 그게 이 실험이다.


1. 기준선 — 배포하기 전에

넓은 것부터 좁혀 간다.

노드 자원 → 네임스페이스에 무엇이 있나 → 이미지가 두 노드에 있나 → Keycloak realm

1-1. 노드에 자원이 있나

확인

free -m
sudo kubectl top nodes

실측01-deploy.txt

=== 배포 전 자원 ===
Mem:           11648        7329         280           4        4377        4319
NAME       CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)
kc-lab-1   115m         5%       2192Mi          44%
kc-lab-2   121m         6%       1324Mi          33%

어디를 봐야 하는가 — 노드 메모리 사용률. 여기서는 44% · 33% 다.

이 결과가 의미하는 것 — BFF 는 JVM 이고 replica 가 2 다. 매니페스트는 requests: 320Mi · limits: 512Mi 로 잡혀 있다. 여유가 없으면 파드가 Pending 이거나 OOM 으로 죽는데, 그걸 「Spring 설정 문제」로 읽게 된다. 배포 전에 한 번 보고 시작한다.

1-2. 네임스페이스에 무엇이 있나

확인

sudo kubectl -n keycloak-lab get all
sudo kubectl -n keycloak-lab get secret,ingress

어디를 봐야 하는가keycloak StatefulSet 과 postgres 가 있고, bff · redis 는 없는 것.

이 결과가 의미하는 것 — 이미 있으면 앞 실험의 잔재이고, 그 위에 배포하면 「내가 만든 것」과 「원래 있던 것」이 섞인다. 있으면 5-1 로 먼저 지운다.

get all 은 워크로드 계열만 보여준다. Secret·PVC·Ingress 는 안 나온다. 그래서 두 줄로 나눠 친다.

1-3. Keycloak realm 을 준비한다

BFF 가 붙을 realm 과 클라이언트가 있어야 한다. 없으면 배포는 성공하는데 로그인에서 막힌다.

하기kcadm 에 로그인한다. Keycloak 이미지 안에 있는 도구다

sudo kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
  config credentials --server http://localhost:8080 --realm master --user admin \
  --password "$(sudo kubectl -n keycloak-lab get secret keycloak-lab-secrets \
                -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)"

비밀번호를 화면에 찍지 않는다. 명령 치환으로 넘기므로 터미널에도 셸 히스토리에도 값이 남지 않는다. 길이만 보고 싶으면:

sudo kubectl -n keycloak-lab get secret keycloak-lab-secrets \
  -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c

실측19

하기 — realm 과 클라이언트

sudo kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
  create realms -s realm=keycloak-patterns -s enabled=true -s accessTokenLifespan=60

sudo 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 'redirectUris=["https://app1.hyeonworks.com/*"]'

되돌리기

sudo kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
  delete realms/keycloak-patterns

어디를 봐야 하는가accessTokenLifespan=60.

이 결과가 의미하는 것B-3(refresh 경쟁)을 위해 미리 짧게 잡는 것이다. 만료를 기다리는 시간이 짧아야 재현이 된다. 지금 정해 두면 나중에 realm 을 다시 안 만든다.

확인 — 만들어졌나

sudo kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
  get realms/keycloak-patterns --fields realm,enabled,accessTokenLifespan

로그인할 사용자도 하나 만든다.

sudo kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
  create users -r keycloak-patterns -s username=labuser -s enabled=true
sudo 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'

★ 이 비밀번호는 브라우저에 직접 칠 것이므로 당신이 정한다. 위 값은 예시고, 실제로 쓸 값은 셸 히스토리에 남지 않게 하려면 kcadm.sh 를 대화식으로 쓰거나 나중에 관리 콘솔에서 바꾼다.


2. 준비 — 배포에서 겪은 문제 다섯 가지를 먼저 읽는다

이 절을 건너뛰면 다섯 번 막힌다. 전부 이 실험대가 실제로 겪은 것이다.

2-1. B-0 상태로 되돌린다 — 네 파일

현재 소스는 B-1·B-2 의 결과를 담고 있다. B-0 을 재려면 그 배선을 빼야 한다.

되돌리기 — 실험이 끝나면 원래대로 돌린다

git checkout -- bff/pom.xml bff/src/main/resources/application.yml \
  bff/src/main/java/com/example/keycloakpattern/bff/SecurityConfig.java \
  deploy/lab/k8s/bff-redis.yaml

하기 ①bff/pom.xml 에서 두 블록을 지운다

vim bff/pom.xml
지울 의존성
spring-session-data-redis 있으면 SessionRepository 가 Redis 로 갈린다 (B-1)
spring-boot-starter-data-redis 있으면 Redis 연결 빈이 잔뜩 생긴다 (B-1)
spring-boot-starter-jdbc · postgresql · h2 B-2 의 JdbcOAuth2AuthorizedClientService

하기 ②SecurityConfig.java 에서 빈 두 개를 지운다

vim bff/src/main/java/com/example/keycloakpattern/bff/SecurityConfig.java
// 지운다 — B-2 가 넣은 것. 이게 있으면 자동구성이 고를 기회가 없다
@Bean
OAuth2AuthorizedClientService authorizedClientService(...) { ... }

// 지운다 — 이것도 직접 만들면 "자동구성이 골랐다" 가 아니다
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(...) { ... }

관련 import (JdbcOAuth2AuthorizedClientService, JdbcOperations, 매니저 계열)도 같이 지운다. bffSecurity 빈은 남긴다/actuator/** 를 열어 주는 것이 그 안에 있다(문제 ④).

하기 ③application.yml 에서 세 블록을 지운다

vim bff/src/main/resources/application.yml
지울 블록
spring.session store-type 기본값이 redis 다. 남겨 두면 의존성만 빼도 경고가 난다
spring.data.redis Redis 연결 설정
spring.datasource · spring.sql.init B-2 의 JDBC 용

하기 ④deploy/lab/k8s/bff-redis.yamlbff 컨테이너에서 env 를 지운다

vim deploy/lab/k8s/bff-redis.yaml
# 지운다 — B-1 · B-2 가 넣은 것
- name: SPRING_SESSION_STORE_TYPE
- name: REDIS_HOST
- name: REDIS_PORT
- name: BFF_DB_URL
- name: BFF_DB_USER
- name: BFF_DB_PASSWORD

Redis Deployment·Service·PVC 는 그대로 둔다. 배포는 하되 연결만 안 한다 — 그게 B-0 의 구성이다.

확인 — 무엇을 지웠는지 눈으로 본다

git diff --stat
git diff bff/src/main/java/com/example/keycloakpattern/bff/SecurityConfig.java

2-2. 문제 ① — 소스 없이 빌드 산출물만 커밋되어 있었다

원래 실행은 bff/ 에 이런 상태를 만났다.

bff/target/classes/...   9개 파일
bff/src/                 없음

.gitignoretarget/ 이 없어 클래스 파일만 커밋되어 있었고 소스는 다른 브랜치에 있었다.

확인 — 지금 당신의 저장소는 어떤가

ls bff/src/main/java/com/example/keycloakpattern/bff/

없으면 가져온다.

git checkout origin/develop-keycloak-pattern3 -- bff/

이 결과가 의미하는 것빌드 산출물이 커밋되어 있으면 「빌드가 되는데 바꿔도 안 바뀐다」가 된다. 소스가 있는지부터 본다.

2-3. 문제 ② — 빌드 실패 원인이 마지막 15줄에 없다

하기

docker build --progress=plain -t keycloak-pattern-bff:lab bff/ > /tmp/build.log 2>&1
echo "exit=$?"

확인 — 실패했으면 전체 로그에서 찾는다

grep -nE "Tests run|Caused by|\.java:[0-9]" /tmp/build.log

실측 — 원래 실행이 만난 것

org.yaml.snakeyaml.constructor.SafeConstructor.processDuplicateKeys

management: 아래에 endpoint: 블록을 하나 더 넣어서 난 오류였다. 이미 있는데 또 넣은 것이다.

이 결과가 의미하는 것docker build 기본 출력은 마지막 몇 줄만 보여준다. Maven 스택트레이스는 그 위에 있다. --progress=plain 으로 전체를 파일로 받고 grep 으로 찾는다.

yamllint 는 이 실험대에 깔려 있지 않다. YAML 중복 키는 빌드가 잡아 준다 — 다만 그 메시지를 보려면 위처럼 해야 한다.

2-4. 문제 ③ — 환경변수에 기본값이 없으면 테스트가 죽는다

# 이러면 테스트에서 컨텍스트가 안 뜬다 — 테스트는 그 환경변수를 모른다
authorization-uri: ${KC_ISSUER_EXTERNAL}/protocol/openid-connect/auth

# 기본값을 준다
authorization-uri: ${KC_ISSUER_EXTERNAL:http://localhost:8080/realms/keycloak-patterns}/protocol/openid-connect/auth

확인 — 지금 파일이 그렇게 되어 있나

grep -n 'KC_ISSUER' bff/src/main/resources/application.yml

2-5. 문제 ④ — actuator 가 인증에 막혀 200 인데 로그인 페이지

/actuator/beans 를 불렀는데 200 이 왔다. 내용은 Keycloak 로그인 페이지였다. -L 로 리다이렉트를 따라간 결과다.

// SecurityConfig 의 permitAll 목록
"/actuator/health",
"/actuator/health/**",
// 실험대 전용 — 운영에서는 절대 열지 않는다
"/actuator/**"

200 이 곧 성공은 아니다. 무엇이 왔는지 봐야 한다. 이 함정은 -o /dev/null -w '%{http_code}' 만 쓸 때 절대 안 보인다.

2-6. 문제 ⑤ — 큰 응답이 프록시에서 Bad Gateway

/actuator/beans117KB 다. nginx → Traefik 을 거치면서 실패했다.

실측experiment-b0-bff-redis-deploy.md 1절

$ curl https://app1.hyeonworks.com/actuator/beans
Bad Gateway

해결 — 파드 안에서 직접 받는다. alpine 기반 JRE 이미지에는 wget 이 있다. (Keycloak 이미지와 다른 점이다 — 거기엔 curl 도 wget 도 없다.)

2-7. 이미지를 두 노드에 밀어 넣는다

레지스트리가 없다. imagePullPolicy: Never 라서 두 노드에 각각 있어야 한다.

하기 — 워크스테이션에서

docker save keycloak-pattern-bff:lab | ssh test-server "ssh kc-lab-1 'sudo k3s ctr images import -'"
docker save keycloak-pattern-bff:lab | ssh test-server "ssh kc-lab-2 'sudo k3s ctr images import -'"

확인 — 두 노드에 들어갔나

sudo k3s ctr images ls | grep keycloak-pattern-bff
ssh kc-lab-2 'sudo k3s ctr images ls | grep keycloak-pattern-bff'

한쪽만 있으면 그 노드에 스케줄된 replica 만 뜬다. ErrImageNeverPull 로 나타난다.


3. 배포

되돌리기 — 먼저 읽어 둔다

sudo kubectl delete -f deploy/lab/k8s/bff-redis.yaml

3-1. 적용

하기

sudo kubectl apply -f deploy/lab/k8s/bff-redis.yaml
sudo kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
sudo kubectl -n keycloak-lab rollout status deployment/bff --timeout=300s

실측01-deploy.txt

=== 배포 ===
secret/bff-secrets created
deployment.apps/redis created
service/redis created
deployment.apps/bff created
service/bff created
ingress.networking.k8s.io/bff created

deployment "redis" successfully rolled out
Waiting for deployment "bff" rollout to finish: 1 of 2 updated replicas are available...
deployment "bff" successfully rolled out

3-2. 배포 구성 — 무엇이 어디에 있나

   브라우저 ──https──▶ nginx ──▶ Traefik ──▶ bff (2 replica)
                                              │
                                              ├──▶ Keycloak  (realm: keycloak-patterns)
                                              └──▶ echo      (resource server 대역)

   redis  ── kc-lab-2 (postgres 와 같은 노드)  ← 아직 연결하지 않았다

Redis 는 배포만 하고 BFF 에 연결하지 않았다. 이 상태를 먼저 재는 것이 B-0 이다.

브라우저용 URL 과 백채널 URL 을 분리한다

authorization-uri: ${KC_ISSUER_EXTERNAL}/protocol/openid-connect/auth   # 브라우저가 간다
token-uri:         ${KC_ISSUER_INTERNAL}/protocol/openid-connect/token  # BFF 가 서버끼리
- name: KC_ISSUER_EXTERNAL
  value: https://auth.hyeonworks.com/realms/keycloak-patterns
- name: KC_ISSUER_INTERNAL
  value: http://keycloak.keycloak-lab.svc:8080/realms/keycloak-patterns

브라우저가 보는 이름과 서버가 부르는 주소는 다르고, 섞으면 리다이렉트가 깨진다. SERVER_FORWARD_HEADERS_STRATEGY=native 도 같은 이유다 — 없으면 Spring 이 redirect_urihttp:// 로 만들어 Keycloak 이 거부한다.


4. 배포가 실제로 걸렸는지 확인한다

4-1. 파드가 두 노드에 하나씩 떴나

확인

sudo kubectl -n keycloak-lab get pods -o wide -l app=bff
sudo kubectl -n keycloak-lab get pods -o wide -l app=redis

실측01-deploy.txt

bff-574c6d658b-8cz4x        true   kc-lab-1
bff-574c6d658b-zpkbp        true   kc-lab-2
redis-568bd7c4-5c5vc        true   kc-lab-2

어디를 봐야 하는가BFF 두 개가 서로 다른 노드에 있는 것.

이 결과가 의미하는 것topologySpreadConstraints 가 일했다. 「다른 인스턴스」가 진짜 다른 기계여야 이 층의 질문이 성립한다. 같은 노드의 다른 프로세스면 재는 의미가 절반이다.

Pending 이면 describe pod 의 Events 를 본다. ErrImageNeverPull 이면 2-7 로 돌아간다.

4-2. 밖에서 닿나

확인

curl -I https://app1.hyeonworks.com/

형태

HTTP/2 200
content-type: text/html

실측02-autoconfiguration.txt

=== 외부 진입점 ===
  https://app1.hyeonworks.com/  HTTP 200

어디를 봐야 하는가 — 상태 줄과 content-type. 2-5 의 함정 때문이다. text/html 이 왔다고 그게 당신의 HTML 이라는 보장은 없다. 다음 절에서 내용까지 본다.

-I 는 헤더만 본다. 여기서는 닿는지를 물어보는 것이라 이 형태가 맞다. 나중에 여러 번 재서 비교할 때는 -o /dev/null -w '%{http_code}' 를 쓴다.


5. 관찰 — 자동구성이 실제로 고른 것

5-1. /actuator/beans 를 파드 안에서 받는다

하기 — 파드 이름을 먼저 잡는다

sudo kubectl -n keycloak-lab get pods -l app=bff
BFF=$(sudo kubectl -n keycloak-lab get pod -l app=bff \
        --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')
echo "$BFF"

하기 — 파드 안에서 받아 파일로 저장한다

sudo kubectl -n keycloak-lab exec "$BFF" -- \
  wget -qO- http://localhost:8083/actuator/beans > /tmp/beans.json
wc -c /tmp/beans.json

형태

119552 /tmp/beans.json

어디를 봐야 하는가 — 크기가 10만 바이트 대인 것. 원래 실행에서 117KB 였다. 0 이면 못 받은 것이고, 몇 백 바이트면 로그인 페이지나 오류 본문이다.

확인 — 진짜 JSON 인지 앞부분을 본다

head -c 200 /tmp/beans.json ; echo

형태

{"contexts":{"keycloak-bff":{"beans":{"actuatorEndpointsSupplier":{"aliases":[],"scope":"singleton","type":"org.springframework

어디를 봐야 하는가{"contexts":{"keycloak-bff" 로 시작하는 것. <!DOCTYPE html 로 시작하면 2-5 의 함정이다 — 로그인 페이지를 받았다.

5-2. jq 없이 빈 목록을 읽는다

빈 하나는 이런 모양이다.

"이름":{"aliases":[],"scope":"singleton","type":"패키지.클래스", ...}

이름과 타입이 이 한 덩어리 안에 같이 있다. 그러니 그 덩어리만 뽑으면 된다.

확인 — 전체 빈 수. 미검증

grep -o '"aliases":\[' /tmp/beans.json | wc -l

실측03-beans-analysis.txt

  컨텍스트: keycloak-bff
  전체 빈 수: 321

확인 — 이름과 타입을 한 줄로. 미검증

grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"' /tmp/beans.json \
  | sed 's/{"aliases".*"type":"/ -> /' \
  | grep -i authorizedclient

형태

"authorizedClientManager" -> org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager
"authorizedClientRepository" -> org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository
"authorizedClientService" -> org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService

어디를 봐야 하는가 — 화살표 오른쪽의 클래스 이름 끝부분.

★ 여기서 원래 실행이 실제로 넘어졌다

실측02-autoconfiguration.txt

  File "<stdin>", line 9
    print(f"    {name:46} {t.rsplit(\".\",1)[-1]}")
                                     ^
SyntaxError: unexpected character after line continuation character

JSON 을 파이썬 한 줄로 파싱하려다 따옴표 이스케이프에서 깨졌다. 빈 목록은 결국 다음 시도에서 나왔고, 그 결과가 03-beans-analysis.txt 다.

jq 가 있으면 그걸 쓴다. 없으면 grep 으로 충분하다. 이 실험대에는 jq 가 없다. 없는 도구를 전제로 한 명령은 진단 도중에 패키지를 깔러 나가게 만든다. 그러지 않으려고 위 형태를 쓴다.

5-3. B-0 의 답

실측03-beans-analysis.txt

  --- 세션 · 토큰 저장소 관련 ---
    authorizedClientManager      -> AuthorizedClientServiceOAuth2AuthorizedClientManager
    authorizedClientManagerRegistrar -> OAuth2ClientConfiguration$OAuth2AuthorizedClientManagerRegistrar
    authorizedClientRepository   -> AuthenticatedPrincipalOAuth2AuthorizedClientRepository
    authorizedClientService      -> InMemoryOAuth2AuthorizedClientService
    clientRegistrationRepository -> InMemoryClientRegistrationRepository

  --- Redis / Spring Session 이 구성되었는가 ---
    ★ 없음 — Redis 도 Spring Session 도 구성되지 않았다

확인 — Redis 와 Spring Session 이 정말 없는지 직접 센다

grep -ci 'RedisSessionRepository\|SpringHttpSessionConfiguration\|LettuceConnectionFactory' /tmp/beans.json

형태

0

어디를 봐야 하는가0.

이 결과가 의미하는 것 — 의존성 자체가 없으니 자동구성이 걸릴 조건이 없다. 세션은 서블릿 컨테이너(Tomcat)의 기본 StandardSession 에 있다.인스턴스 메모리다.

구현체
authorizedClientService InMemoryOAuth2AuthorizedClientService 프로세스 메모리. 재시작하면 사라진다
authorizedClientRepository AuthenticatedPrincipalOAuth2AuthorizedClientRepository principal 기준 조회. session ID 가 없다
authorizedClientManager AuthorizedClientServiceOAuth2AuthorizedClientManager service(공유)를 쓴다
clientRegistrationRepository InMemoryClientRegistrationRepository 설정에서 읽은 것
SessionRepository 없음 Tomcat 의 기본 StandardSession
Redis / Spring Session 없음 의존성 자체가 없다

5-4. ★ 이름 하나가 이 층 전체의 문제다

AuthenticatedPrincipalOAuth2AuthorizedClientRepository이름이 곧 설명이다.

   요청이 인증되어 있으면
     └─▶ OAuth2AuthorizedClientService 에 위임
            └─▶ 키: (clientRegistrationId, principalName)
                        └─ session ID 가 없다   ★
   인증되어 있지 않으면
     └─▶ HttpSession 에 임시 보관

같은 사용자가 두 브라우저에서 로그인하면 principalName 이 같으므로 같은 항목을 본다. 한쪽에서 토큰을 갱신하면 다른 쪽 것을 덮어쓴다.

Redis 를 붙여도 이건 안 고쳐진다. 저장소를 공유해도 키에 session ID 가 없기 때문이다. 「Session Store 를 공유 저장소로 바꾸는 것만으로는 충분하지 않다」의 기제가 이 빈 하나에 들어 있다.

이것이 추측으로는 안 나오는 부분이다. 「메모리겠지」까지는 맞혔어도 조회 키가 무엇인지는 빈 이름을 봐야 안다.


6. ★ 예상 못 한 것 — replica 2개에서 로그인 자체가 안 된다

여기부터는 브라우저로 한다.

6-1. 증상

하기 — 브라우저에서 https://app1.hyeonworks.com/ 을 열고 로그인한다.

형태 — 주소창이 이렇게 끝난다

https://app1.hyeonworks.com/login?error

확인 — 로그를 본다. 두 파드를 다 봐야 한다

sudo kubectl -n keycloak-lab logs -l app=bff --tail=100 --prefix

어디를 봐야 하는가아무 오류도 없다.

이 결과가 의미하는 것 — Spring Security 는 로그인 실패를 DEBUG 로만 남긴다. 「로그에 아무것도 없으니 애플리케이션 문제가 아니다」로 읽으면 틀린다. 증상은 있는데 로그가 없는 상태이고, 그럴 때는 가설을 세워 시험한다.

6-2. 가설

   ① 브라우저 → 앱 → IdP 로 리다이렉트   (state·PKCE verifier 를 저장)
   ② IdP → 브라우저 → 앱의 콜백          (저장한 것을 꺼내 검증)

인가 코드 흐름은 왕복이 두 번이고, 두 번 다 같은 인스턴스로 가야 한다. 저장 위치가 HttpSession 이고 그게 인스턴스 메모리이므로, 콜백이 다른 replica 로 가면 저장된 인가 요청이 없어 실패한다.

5-3 에서 본 「SessionRepository 없음」이 이 가설의 근거다.

6-3. 검증 — replica 를 1로 줄인다

되돌리기 — 먼저 읽어 둔다

sudo kubectl -n keycloak-lab scale deployment/bff --replicas=2

하기

sudo kubectl -n keycloak-lab scale deployment/bff --replicas=1
sudo kubectl -n keycloak-lab rollout status deployment/bff --timeout=180s
sudo kubectl -n keycloak-lab get pods -l app=bff

하기 — 브라우저에서 다시 로그인한다. 쿠키를 먼저 지운다 (앞선 실패의 세션이 남아 있으면 결과가 섞인다).

실측b0-bff-login-success-single-replica.png

   replica 2 + 스티키 없음 → 로그인 실패 (콜백이 다른 인스턴스로)
   replica 1              → 로그인 성공

이 결과가 의미하는 것가설 확정.

「다중 인스턴스에서 어떻게 운영할 것인가」는 로그인한 뒤의 문제가 아니라 로그인 자체의 문제다. B-2 의 검증 1번(「한쪽에서 로그인한 뒤 다른 인스턴스로 요청」)보다 앞선 단계다. 로그인이 끝나야 그 검증을 할 수 있는데, 로그인부터 막힌다.

6-4. 토큰 경계 — 브라우저에 무엇이 있나

하기 — 브라우저에서 https://app1.hyeonworks.com/bff/token-boundary

실측b0-bff-token-boundary.png

{"pattern":"AP3-backend-for-frontend","principal":"labuser",
 "accessTokenStoredOnServer":true,"refreshTokenStoredOnServer":true,
 "browserTokenCount":0,"csrfProtectionEnabled":true}

어디를 봐야 하는가 — 세 값.

필드
accessTokenStoredOnServer true 서버가 access token 을 들고 있다
refreshTokenStoredOnServer true refresh token 도 서버에 있다
browserTokenCount 0 브라우저에는 토큰이 하나도 없다

이 결과가 의미하는 것BFF 패턴이 성립한다. 브라우저는 세션 쿠키만 들고 있고 토큰은 전부 서버에 있다. 이 세 값이 B-1 에서 어떻게 바뀌는지가 다음 실험의 요지다. 지금 값을 적어 둔다.


7. 복구

7-1. replica 를 되돌린다

하기

sudo kubectl -n keycloak-lab scale deployment/bff --replicas=2
sudo kubectl -n keycloak-lab rollout status deployment/bff --timeout=180s

B-1 로 이어서 갈 것이라면 배포는 그대로 둔다. 거기서 같은 파드에 Redis 를 붙인다.

7-2. 소스 변경을 되돌린다

하기

git checkout -- bff/pom.xml bff/src/main/resources/application.yml \
  bff/src/main/java/com/example/keycloakpattern/bff/SecurityConfig.java \
  deploy/lab/k8s/bff-redis.yaml
git status --short

★ 잊으면 다음에 apply 할 때 B-0 구성이 다시 배포된다.

7-3. 전부 지울 때

하기

sudo kubectl delete -f deploy/lab/k8s/bff-redis.yaml
sudo kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kcadm.sh \
  delete realms/keycloak-patterns

★ PVC 는 delete -f 로 같이 지워진다. Redis 데이터도 사라진다.

7-4. 원상복구 확인표

항목 명령 돌아왔을 때
replica sudo kubectl -n keycloak-lab get deploy bff 2/2
소스 git status --short 출력 없음
파드 sudo kubectl -n keycloak-lab get pods -o wide -l app=bff 두 노드에 하나씩
Keycloak sudo kubectl -n keycloak-lab get pods -o wide | grep keycloak 둘 다 1/1 Running
curl -I https://app1.hyeonworks.com/ 200
임시 파일 rm -f /tmp/beans.json /tmp/build.log

★ actuator 를 열어 둔 채로 두지 않는다. /actuator/beans/actuator/env내부 구조와 설정값을 그대로 드러낸다. 실험대라서 여는 것이고, 운영이라면 health 만 남긴다.

이 실험이 재지 않은 것 — 스티키 세션(세션 어피니티)을 켜면 replica 2 에서 로그인이 되는지는 재지 않았다. 「같은 인스턴스로 보내면 된다」는 추론이지 측정이 아니다.


막히면

전부 이 실험대가 실제로 겪은 증상이다. 지어낸 것은 없다.

증상 원인 확인
빈 목록에 RedisSessionRepository 가 있다 B-1·B-2 배선이 남아 있다 2-1 을 다시. git diff 로 확인
authorizedClientServiceJdbc... SecurityConfig 의 명시 빈이 남아 있다 2-1 하기 ②
/actuator/beansBad Gateway 응답이 117KB 라 프록시가 못 넘긴다 파드 안에서 받는다 — 5-1
/actuator/beans200 인데 HTML Keycloak 로그인 페이지다 head -c 200 으로 내용 확인 — 5-1
beans.json 이 0 바이트 파드 이름이 틀렸거나 포트가 다르다 get pods -l app=bff, 포트는 8083
빌드가 실패하는데 원인이 안 보인다 마지막 15줄에 없다 --progress=plain + 파일 — 2-3
테스트에서 컨텍스트가 안 뜬다 환경변수에 기본값이 없다 2-4
파드가 ErrImageNeverPull 그 노드에 이미지가 없다 두 노드에 각각 import — 2-7
파드가 Pending 노드 메모리 부족 describe pod Events, top nodes — 1-1
브라우저가 /login?error replica 2 + 스티키 없음 replica 1 로 줄여 확인 — 6-3
BFF 로그에 오류가 없다 Spring Security 는 로그인 실패를 DEBUG 로만 남긴다 로그 없음을 「문제 없음」으로 읽지 않는다 — 6-1
로그인 후 리다이렉트가 http:// 로 간다 SERVER_FORWARD_HEADERS_STRATEGY 가 없다 매니페스트 env 확인 — 3-2
jq: command not found 이 실험대에 jq 가 없다 grep 으로 읽는다 — 5-2
kcadm401 config credentials 를 안 했거나 만료됐다 1-3 을 다시

다음

실험 B-0 이 남긴 것
B-1 저장소 결정 전환 후 이 빈들을 다시 찍는다. 「Redis 붙였다」고 믿는데 자동구성이 안 걸리는 경우가 흔하다
B-2 다중 인스턴스 로그인 자체가 실패한다는 것이 이미 관측됐다. 그게 검증 0번이다
B-3 refresh 경쟁 accessTokenLifespan=60 으로 realm 을 만들어 뒀다
운영 actuator beans/env내부 구조를 그대로 드러낸다. 실험대에서만 연다