Files
document-haness/docs/keycloak-session-store/tech-log-studio/where-application-state-lives/setup/setup-reproduce-b1-redis-session-store.md
T
DongHyeonkaandClaude Opus 5 32e39e20aa fix(setup): 실험대에서 35편을 끝까지 밟고 어긋난 명령·결과 31건을 고친다
test-server 를 비우고 다시 세운 뒤 Setup 기록 35편(virtualization 9 ·
keycloak-session-store 26)을 문서에 적힌 명령 그대로 쳤다. 어긋난 자리를
기록과 SSOT 양쪽에 실측과 함께 넣었다.

막히던 것
- 04 의 인증서 경로가 live/hyeonworks.com 이라 nginx 가 [emerg] 로 안 떴다.
  실제 계보는 live/auth.hyeonworks.com 이고 「문제가 생기면」은 진단이 거꾸로였다
- 인증서가 와일드카드가 아니다. SAN 이 auth·app1·app2 셋뿐이라 그 밖의 이름은
  TLS 에서 끊기고 curl 이 exit 60 · %{http_code} 000 을 낸다. SSOT 안에서
  두 문단이 서로 어긋나 있었다
- A-7 14번 ①이 kc-lab-1 에서 여섯 줄 다 실패하는데 마지막 date 만 「차단」을 찍는다

검사가 실패할 수 없던 자리
- B-1 의 세션 키 고르기는 앞 단계가 $KEY 를 채워 둬서 루프가 한 건도 못 맞혀도
  통과한다. KEY= 로 비우고 키마다 1/0 을 찍게 바꿨다
- k3s-agent 유닛의 sed -i 는 패턴에 $HOME 이 들어 있어 아무 줄도 안 바꾼 채 성공한다

certbot
- renew --dry-run 의 종료 코드는 성공도 0, 실패도 0, 다른 사유의 실패는 1 이다.
  본문의 renew failure(s) 로만 판정할 수 있다
- --dry-run 은 staging 서버를 쓰는데 renewal/*.conf 의 account= 는 운영 계정을
  가리킨다. 실패한 dry-run 이 staging 계정을 하나 더 만들어 다음 실행이 계속 멎는다
- 훅을 755 로 놓고 시뮬레이션이 성공해도 Running deploy-hook command 는 안 나온다.
  certbot 2.1.0 에는 --run-deploy-hooks 도 없다
- 강제 갱신은 실제로 쳤고 서빙까지 닿았다. serial 06F3E0EF…1373 → 065547…3DF1,
  notAfter Dec 3 → Dec 16, nginx worker 2629 4712 → 4745 4754

독자가 칠 수 있는 형태로
- 안 되는 형태가 번호 붙은 단계에 앉아 있던 8곳을 뒤집고, 되는 형태를 ①로 올렸다
- 랩 안에서 공개 이름을 치는 curl 65줄에 --resolve 를 붙였다. 붙인 형태를 실제로
  쳐서 문서가 적은 값과 같은지 확인했다
- 힙독·sed -i·echo >>·&&·|| 를 편집기 + 파일 리스팅 + 분할 형태로 바꿨다
- 닫는 코드펜스가 빠져 뒤 200여 줄의 블록 종류가 뒤집혀 있던 곳을 포함해 3곳을 고쳤다

관문: check_body PASS · check_prose error 0 · check_evidence 두 프로젝트 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0 · 코드펜스 전수 0건

남은 것: B-0 주입은 keycloak-pattern 저장소의 소스를 고치고 이미지를 다시 구워야
해서 안 했다(unknown).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:38:12 +09:00

47 KiB
Raw Blame History

id, kind, slug, title, topic, topicName, project, status, studio, pinnedVersions, source, sourceRevision
id kind slug title topic topicName project status studio pinnedVersions source sourceRevision
1e5d05fa-88f4-4ef5-a402-4a520ae4a52d SETUP reproduce-b1-redis-session-store Redis 를 붙이고 무엇이 옮겨졌는지 빈 목록으로 견준다 where-application-state-lives 세션과 토큰을 Redis 와 PostgreSQL 에 나눠 두기 keycloak-session-store 게시 전 https://hyeonworks.com/studio/documents/1e5d05fa-88f4-4ef5-a402-4a520ae4a52d/edit
name version
keycloak-pattern-bff lab
name version
Redis 7.4.x
final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-1
cdac9b8178391311d8eca1ebc6cac15bb62d79af

Redis 를 붙이고 무엇이 옮겨졌는지 빈 목록으로 견준다

Redis 를 세션 저장소로 붙이고 B-0 에서 찍어 둔 빈 목록과 견주는 절차다. 의존성 둘을 함께 넣어 다시 빌드하고 두 노드에 밀어 넣은 뒤, 무엇이 옮겨졌는지와 무엇이 안 옮겨졌는지를 같은 명령으로 확인한다. 전 구간 약 40분이고 빌드 시간이 들어 있다.

관계

  • 세션만 Redis 로 옮기자 토큰이 따라오지 않았다 이 절차가 낸 결과를 담은 기록이다. 무엇을 발견했는지는 그쪽에 있고 여기에는 치는 순서만 있다.
  • 주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다 의존성을 하나만 넣으면 오류 없이 메모리에 남는다. 그 아홉 건 가운데 하나가 이 편에서 나왔다.
  • 세션과 인가된 클라이언트는 조회 키가 다르다 빈 81개가 늘었는데 인가된 클라이언트 빈 셋이 그대로인 까닭을 그 기록이 설명한다.
  • 아무 저장소도 주지 않고 Spring 이 무엇을 고르는지 찍어서 확인한다 먼저 해 둬야 하는 편이다. 그 편이 남긴 빈 세 개의 이름과 숫자 321 이 여기서 대조군이 된다.
  • 토큰을 PostgreSQL 로 옮기고 기본키와 로그아웃 정리를 확인한다 다음 편이다. 여기서 Redis 로 안 옮겨진 인가된 클라이언트를 그 편이 공유 저장소로 옮긴다.

본문

읽기 전에 — 어디서 치는가

B-0 과 같다. 소스를 고치고 이미지를 만드는 일은 워크스테이션에서 하고, 클러스터를 보고 배포하는 일은 [lab host] 에서 친다. kubectlsudo 를 붙이지 않는다. 브라우저 창도 하나 열어 둔다.

원 가이드는 이 명령들을 kc-lab-1 에서 치라고 적었다. 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 sudo 없는 kubectlpermission denied 로 막힌다 — kubeconfig 는 lab host 의 ~/.kube/config 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 kubectl 블록의 기계 이름을 [lab host] 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.

끊기는 곳도 B-0 과 같다. 시작 전에 셋을 스스로 정해 둔다 — 그 명령이 원 가이드에 없다(unknown).

첫째, 워크스테이션과 kc-lab-1 을 여섯 번 오가는데 건너가는 sshexit 도 이 절차에 없다. 경로는 이미지를 밀어 넣는 줄 하나에만 드러난다 — ssh test-server "ssh kc-lab-1 '...'".

둘째, bff/pom.xmldeploy/lab/k8s/bff-redis.yaml 이 전부 저장소 상대경로다. 두 기계 각각에서 저장소 루트로 옮겨 두고 시작한다.

셋째, 워크스테이션에서 고친 deploy/lab/k8s/bff-redis.yamlkc-lab-1 로 넘기는 단계가 없다. 이 편에서는 그 누락이 B-0 보다 더 헷갈리게 나온다 — 주입 3번이 넣는 enableServiceLinks: falsekc-lab-1 쪽 파일에 없으면 apply 는 성공하는데 파드가 똑같이 CrashLoopBackOff 로 남고, 그때 이 편은 「파드가 아직 안 바뀌었다」를 먼저 의심하라고 적는다. 실제로는 고친 파일이 그 기계에 안 간 것이다.

아래 코드블록의 라벨은 그 줄을 치는 기계를 가리킨다. [워크스테이션 → kc-lab-1] 이 붙은 블록은 한 블록 안에서 기계가 바뀌므로 어느 줄이 어디인지 블록 뒤에 적어 두었다.

무엇
네임스페이스 keycloak-lab
고치는 파일 넷 — bff/pom.xml · application.yml · BffControllerTest.java · bff-redis.yaml
주입 수단 의존성 둘을 넣고 다시 빌드해 두 노드에 import
무엇을 찍나 /actuator/beans 의 빈 수와 이름, Redis 의 키·필드·TTL
브라우저 필요하다. 인가 코드 흐름은 왕복이 두 번이라 curl 로 대신할 수 없다
도구 jq 가 이 실험대에 없다. grepredis-cli 로 읽는다
걸리는 시간 약 40분. 빌드 시간이 들어 있다

이 실험이 가르는 것

B-0 이 답을 냈다. 세션도 토큰도 인스턴스 메모리에 있고, 그래서 replica 2 에서는 로그인조차 안 된다. 처방은 뻔해 보인다 — 공유 저장소를 붙인다.

   Redis 를 붙인다  →  상태가 공유된다  →  다중 인스턴스가 된다
                            ↑
                     정말 그런가?

이 실험이 재는 것은 붙였다와 공유된다 사이의 거리다. 묻는 것이 넷이고 그중 둘째를 이 실험이 판정한다.

물어볼 것
무엇이 옮겨졌나 /actuator/beans 를 다시 찍는다
무엇이 안 옮겨졌나 같은 곳. 안 바뀐 것을 확인하는 쪽이 더 중요하다
옮겨진 것 안에 무엇이 들었나 Redis 를 직접 연다
사용자에게는 어떻게 보이나 브라우저

절차를 끝까지 밟으면 일곱을 자기 화면에서 보게 된다 — 쿠버네티스가 넣지도 않은 환경변수로 파드를 죽이는 것, 그것이 enableServiceLinks: false 로 고쳐지는 것, 빈이 321 에서 402 로 81개 늘어나는 것, 그런데 인가된 클라이언트는 하나도 안 바뀐 것, Redis 안의 키와 필드와 TTL 에 토큰이 없는 것, 세션이 Java 네이티브 직렬화인 것, 로그인은 되어 있는데 아무것도 못 하는 상태.

전제와 되돌리기

  • B-0 이 끝나 있고, B-0 의 답인 빈 세 개의 이름을 손에 들고 시작한다. 이 실험은 그 값들이 어떻게 바뀌는지를 잰다.
  • B-0 의 복구 절 §3 「전부 지운다」를 치지 않은 상태여야 한다. 그 절은 bffredis Deployment 와 realm keycloak-patterns 를 함께 없애므로, 쳤으면 아래 첫 확인부터 빈 목록이 나온다. 그 상태라면 B-0 의 주입 절과 realm·사용자 단계를 다시 밟아 배포를 세운 뒤 여기로 온다.
  • 브라우저가 필요하다.
  • 명령은 kc-lab-1 에서 친다.
  • jq 는 이 실험대에 깔려 있지 않다. 이 절차는 grepredis-cli 로 읽는다.

애플리케이션 구성을 바꾸는 실험이라 의존성과 설정을 고쳐 다시 빌드하고 다시 배포한다. 되돌리려면 소스를 되돌린 뒤 또 한 번 빌드해 두 노드에 다시 밀어 넣어야 하므로 git status 가 깨끗한 상태에서 시작한다.

되돌리기는 먼저 읽어 둔다.

git checkout -- bff/pom.xml bff/src/main/resources/application.yml \
  deploy/lab/k8s/bff-redis.yaml

주입 절의 첫 배포는 일부러 고장 난 상태로 한다. 함정을 직접 보기 위해서이고, 건너뛰고 enableServiceLinks: false 부터 시작해도 결과는 같다고 원 가이드가 적는다.

주입 전에 같은 명령으로 먼저 본다

넓은 것부터 좁혀 간다.

BFF 가 돌고 있나 → B-0 의 답 세 개 → Redis 가 비어 있나 → ★ 파드 안 환경변수

1. BFF 두 개와 Redis 가 떠 있는가

무엇을 보는가 — 파드 배치.

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

어디를 보나 — 모양은 이렇고 주소와 해시는 환경마다 다르다(observed).

bff-574c6d658b-8cz4x   1/1   Running   0   20m   10.42.0.51   kc-lab-1
bff-574c6d658b-zpkbp   1/1   Running   0   20m   10.42.1.52   kc-lab-2
redis-568bd7c4-5c5vc   1/1   Running   0   20m   10.42.1.53   kc-lab-2

이 값이 뜻하는 것 — BFF 두 개가 서로 다른 노드에 있고 Redis 가 떠 있다. Redis 는 배포만 되어 있고 아직 연결되지 않았다. B-0 이 그렇게 만들어 뒀다.

2. B-0 의 답을 before 값으로 다시 잡는다

무엇을 보는가 — 주입 뒤에 견줄 빈 수와 빈 이름 셋.

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" -- \
  wget -qO- http://localhost:8083/actuator/beans > /tmp/beans-before.json
wc -c /tmp/beans-before.json

이 실험대는 jq 가 없어 grep 으로 덩어리를 뽑았다. 원 가이드가 아래 두 줄을 미검증으로 표시했다(unknown).

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":"/ -> /' \
  | grep -iE 'authorizedclient|sessionRepository'

jq 가 깔려 있으면 그것을 쓴다고 원 가이드가 적는데 어떤 표현을 쓰라고는 여기서도 적지 않았다(unknown).

어디를 보나 — 실측은 이렇다(observed, 02-autoconfig-after.txt).

  빈 수: 321 → 402  (+81)
    authorizedClientService
      before: InMemoryOAuth2AuthorizedClientService
    authorizedClientRepository
      before: AuthenticatedPrincipalOAuth2AuthorizedClientRepository
    authorizedClientManager
      before: AuthorizedClientServiceOAuth2AuthorizedClientManager

이 값이 뜻하는 것 — 지금 빈 수가 321 이고 sessionRepository 는 아예 없다. 이 세 줄과 숫자를 적어 둔다. 관찰 절이 이 값과 견주고, 견줄 것이 없으면 안 바뀌었다고 말할 수 없다.

3. Redis 가 살아 있고 비어 있는가

무엇을 보는가 — 연결 여부와 키 개수.

kubectl -n keycloak-lab exec deploy/redis -- redis-cli ping
kubectl -n keycloak-lab exec deploy/redis -- redis-cli info server | head
kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan

어디를 보나 — 모양은 이렇다(observed).

PONG
# Server
redis_version:7.4.x
...
(integer) 0

이 값이 뜻하는 것0 이어야 뒤에서 찾은 키를 내가 만든 것이라고 말할 수 있다. KEYS * 대신 --scan 을 쓰는 까닭은 KEYS 가 서버를 블로킹하기 때문이다. 지금은 키가 0개라 차이가 없지만, 습관이 되면 운영에서 사고가 난다.

4. 파드 안에 이미 Redis 관련 환경변수가 있는가

무엇을 보는가 — 아직 아무것도 안 바꿨는데 들어와 있는 값. 이 실험의 함정이 여기서 시작된다.

한 번은 통째로 보고 그다음 걸러 본다.

kubectl -n keycloak-lab exec "$BFF" -- printenv | sort
kubectl -n keycloak-lab exec "$BFF" -- printenv | grep -i redis

어디를 보나 — 모양은 이렇다(observed).

REDIS_SERVICE_HOST=10.43.57.116
REDIS_SERVICE_PORT=6379
REDIS_PORT=tcp://10.43.57.116:6379
REDIS_PORT_6379_TCP=tcp://10.43.57.116:6379
REDIS_PORT_6379_TCP_ADDR=10.43.57.116
REDIS_PORT_6379_TCP_PORT=6379
REDIS_PORT_6379_TCP_PROTO=tcp

이 값이 뜻하는 것REDIS_PORT 의 값이 포트 번호가 아니라 URL 이다. 쿠버네티스는 같은 네임스페이스의 모든 Service 마다 옛 Docker 링크 호환용 환경변수를 파드에 자동으로 넣고, 그 기능이 기본으로 켜져 있다. Service 이름이 redis 이므로 REDIS_* 가 들어오는데 애플리케이션 설정도 ${REDIS_PORT:6379} 를 읽는다. 이름이 겹친다.

   Service 이름이 redis 이면
     REDIS_SERVICE_HOST=10.43.57.116
     REDIS_SERVICE_PORT=6379
     REDIS_PORT=tcp://10.43.57.116:6379        ← 이게 문제

매니페스트에 REDIS_PORT: "6379" 를 명시하면 그쪽이 이긴다. 명시를 안 하면 자동 주입이 이기고, 오류 메시지는 쓰지도 않은 값을 지목한다. REDISPOSTGRESMYSQL 처럼 흔한 Service 이름일수록 위험하다고 원 가이드가 적는다. 지금은 애플리케이션이 그 변수를 안 읽으므로 아무 일도 안 일어나고, 읽기 시작하는 순간 파드가 죽는다.

주입

1. 의존성 둘을 함께 넣는다

목적SessionRepository 를 Redis 로 갈아끼우고 연결을 제공한다. 하나만 넣으면 오류 없이 메모리에 남으므로 둘을 같이 넣는다.

무엇을 왜 넣는지 읽으면서 고쳐야 하는 파일이라 편집기로 연다.

vim bff/pom.xml
<!-- spring-session-data-redis 가 SessionRepository 를 갈아끼우고,
     spring-boot-starter-data-redis 가 연결(Lettuce)을 제공한다.
     둘 다 있어야 자동구성이 걸린다 -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

설정에 Redis 연결과 세션 저장소를 적는다.

vim bff/src/main/resources/application.yml
spring:
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
  session:
    store-type: ${SPRING_SESSION_STORE_TYPE:redis}
    timeout: ${SPRING_SESSION_TIMEOUT:30m}
    redis:
      namespace: bff:session

테스트에는 Redis 가 없으므로 테스트에서만 저장소를 끈다.

vim bff/src/test/java/com/example/keycloakpattern/bff/BffControllerTest.java
@SpringBootTest(properties = {
    "KEYCLOAK_CLIENT_SECRET=test-only-secret",
    // 테스트는 Redis 를 띄우지 않는다
    "spring.session.store-type=none",
})

예상 결과 — 세 파일이 git diff --stat 에 나온다.

왜 필요한가spring-session-data-redis 를 넣으면 컨텍스트가 뜰 때 Redis 에 붙으려 하고, 테스트에는 Redis 가 없다. spring.session.store-type=none 한 줄이 없으면 빌드가 테스트 단계에서 죽는데 그 실패 메시지가 Redis 연결 오류라 배포 환경 문제로 읽히기 쉽다. 실패한 곳은 빌드다.

문제가 생기면 — 뒤에서 sessionRepository 가 안 생기면 의존성을 하나만 넣은 것이므로 pom.xml 에 둘 다 있는지부터 본다.

2. 매니페스트를 일부러 고장 난 채로 올린다

목적 — 자동 주입된 REDIS_PORT 가 애플리케이션 설정을 어떻게 이기는지 한 번 본다.

vim deploy/lab/k8s/bff-redis.yaml
spec:
  # enableServiceLinks: false     ← 아직 넣지 않는다
  containers:
    - name: bff
      env:
        - name: SPRING_SESSION_STORE_TYPE
          value: redis
        - name: REDIS_HOST
          value: redis.keycloak-lab.svc
        # REDIS_PORT 를 일부러 안 준다 — 자동 주입이 어떻게 이기는지 본다

빌드해서 두 노드에 밀어 넣고 배포한다. 아래 네 줄이 이 실험대가 실제로 친 형태다(observed).

docker build --progress=plain -t keycloak-pattern-bff:lab bff/ > /tmp/build.log 2>&1
echo "exit=$?"
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 -'"
kubectl apply -f deploy/lab/k8s/bff-redis.yaml
kubectl -n keycloak-lab rollout restart deployment/bff

이 블록은 한 블록인데 기계가 둘이다. 앞의 네 줄(docker build · echo · docker save 두 줄)은 워크스테이션에서 치고, kubectl 로 시작하는 아래 두 줄은 kc-lab-1 에서 친다. 워크스테이션에는 kubeconfig 가 없어 거기서 kubectl 을 치면 클러스터에 못 붙고 끝난다. 그리고 kubectl apply 가 읽는 deploy/lab/k8s/bff-redis.yaml 은 방금 워크스테이션에서 고친 그 파일이므로, kc-lab-1 쪽 체크아웃에도 같은 내용이 있어야 한다. 옮기는 명령은 원 가이드에 없다(unknown).

가운데 두 줄은 한 줄에 ssh 가 두 겹이고 원격 셸의 인용이 겹쳐 있어 나눠 치고 싶어지는데, 원 가이드가 나눈 형태를 적어 두지 않아 여기에도 없다(unknown).

지금 멈추려면 롤아웃을 되돌린다.

kubectl -n keycloak-lab rollout undo deployment/bff

예상 결과 — 파드가 뜨지 않는다. 넓은 것부터 본다.

kubectl -n keycloak-lab get pods -l app=bff
NAME                   READY   STATUS             RESTARTS      AGE
bff-695646ddb-kzs9k    0/1     CrashLoopBackOff   3 (20s ago)   90s

로그보다 먼저 이벤트를 보고, 그다음 로그를 본다. 지금 파드와 죽기 전 파드를 따로 본다.

kubectl -n keycloak-lab describe pod -l app=bff | tail -20
kubectl -n keycloak-lab logs -l app=bff --tail=40
kubectl -n keycloak-lab logs -l app=bff --previous --tail=40

실측은 이렇다(observed, 해설 문서 1절).

Failed to bind properties under 'spring.data.redis.port' to int:
    Property: spring.data.redis.port
    Value: "${REDIS_PORT:6379}"
    Reason: failed to convert java.lang.String to int
            (caused by NumberFormatException: For input string: "tcp://10.43.57.116:6379")

왜 필요한가 — 마지막 줄의 "tcp://10.43.57.116:6379" 는 매니페스트 어디에도 쓰지 않은 값이다. 주입 전에 printenv 로 미리 본 그 환경변수이고 쿠버네티스가 넣었다. Redis 는 멀쩡하고, 파드는 Redis 에 붙어 보지도 못한 채 설정 바인딩에서 죽었다. 메시지가 Failed to bind properties 라고 말하고 있다.

kubectl -n keycloak-lab exec deploy/redis -- redis-cli ping

PONG 이 온다(observed).

문제가 생기면 — 파드가 정상으로 떴다면 매니페스트에 REDIS_PORT 를 이미 줬다. 함정을 건너뛰어도 결과는 같으므로 다음 단계로 간다.

3. 자동 주입을 끄고 다시 올린다

목적 — 이름 충돌의 근본을 없앤다.

처방은 둘인데 하나만 근본 처방이다.

처방 문제
환경변수 이름을 바꾼다 (BFF_REDIS_PORT 등) 다음 사람이 같은 함정에 다시 빠진다
주입 자체를 끈다 근본 처방
vim deploy/lab/k8s/bff-redis.yaml
spec:
  enableServiceLinks: false      # 근본 처방
  containers:
    - name: bff
      env:
        - name: SPRING_SESSION_STORE_TYPE
          value: redis
        - name: REDIS_HOST
          value: redis.keycloak-lab.svc
        - name: REDIS_PORT
          value: "6379"
kubectl apply -f deploy/lab/k8s/bff-redis.yaml
kubectl -n keycloak-lab rollout status deployment/bff --timeout=300s

예상 결과 — 실측은 이렇다(observed, 01-servicelinks-trap.txt).

deployment.apps/bff configured
deployment "bff" successfully rolled out
bff-576d869c6d-bshvl   true   kc-lab-2
bff-695646ddb-kzs9k    true   kc-lab-1
bff-695646ddb-vjqzf    true   kc-lab-2

세 줄이다. replica 는 2인데 파드가 3개 보이는 것은 롤아웃 전환 중에 찍어서이고, 옛 ReplicaSet 의 파드가 아직 종료 전이다. 잠시 뒤 두 개가 된다.

왜 필요한가enableServiceLinks: false 는 그 파드에 대해 Service 이름 기반 환경변수 주입 전체를 끈다. 이름 하나를 피해 가는 것과 달라서 다음에 Service 를 하나 더 만들어도 같은 충돌이 안 난다.

문제가 생기면 — 고쳤는데 오류가 똑같이 나면 파드가 아직 안 바뀌었다. rollout restart 뒤에 printenv 를 다시 본다.

주입 검증

결과를 읽기 전에 주입이 의도한 것을 정확히 했는지 먼저 본다. 자동 주입이 정말 사라졌는지부터다.

1. 자동 주입된 환경변수가 사라졌는가

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

모양은 이렇다(observed).

REDIS_HOST=redis.keycloak-lab.svc
REDIS_PORT=6379

REDIS_SERVICE_HOST 계열이 전부 사라졌고 넘겨준 두 개만 남았다. REDIS_PORT6379 다.

2. 자동구성이 실제로 걸렸는가

kubectl -n keycloak-lab get pods -o wide -l app=bff
kubectl -n keycloak-lab exec "$BFF" -- \
  wget -qO- http://localhost:8083/actuator/health

모양은 이렇다(observed).

{"status":"UP","components":{"redis":{"status":"UP","details":{"version":"7.4.x"}},...}}

redis 컴포넌트가 있고 UP 이다. B-0 에서는 이 컴포넌트가 아예 없었다. spring-boot-starter-data-redis 가 헬스 인디케이터를 같이 들고 왔고, 건강 체크에 새 항목이 생긴 것이 자동구성이 걸렸다는 신호다.

3. replica 2 에서 로그인이 되는가

브라우저에서 쿠키를 먼저 지우고 https://app1.hyeonworks.com/ 로 로그인한다.

로그인이 된다(observed, b1-login-works-two-replicas.png). B-0 에서 replica 2 로는 /login?error 였던 그 부분이다. 인가 요청의 state 와 PKCE verifier 가 이제 Redis 에 있으므로 콜백이 다른 인스턴스로 가도 찾을 수 있다. B-0 이 replica 를 1 로 줄여야 했던 문제는 고쳐졌다. 원 가이드는 여기에 곧바로 경고를 붙인다 — 여기서 멈추면 Redis 를 붙였더니 다 해결됐다로 끝나고, 그것이 이 실험이 막으려는 결론이다.

관찰

1. 빈 목록을 다시 찍어 before 와 견준다

무엇을 보는가 — 늘어난 빈과 안 바뀐 빈.

B-0 의 방법을 그대로 다시 쓴다.

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

어디를 보나 — 실측은 이렇다(observed, 02-autoconfig-after.txt).

  빈 수: 321 → 402  (+81)

새로 생긴 세션 저장소 빈을 뽑는다. 원 가이드가 아래 줄을 미검증으로 표시했다(unknown).

grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"' /tmp/beans-after.json \
  | sed 's/{"aliases".*"type":"/ -> /' \
  | grep -iE 'session|redis'
  --- 세션 저장소 관련 (새로 생긴 것) ---
    ★ cookieSerializer                           -> DefaultCookieSerializer
    ★ org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration -> RedisHttpSessionConfiguration
    ★ sessionRepository                          -> RedisSessionRepository
    ★ springSessionRepositoryFilter              -> SessionRepositoryFilter
    ★ redisConnectionFactory                     -> LettuceConnectionFactory
    ★ redisTemplate                              -> RedisTemplate

같은 파일에서 인가된 클라이언트 쪽을 따로 뽑는다. 이 실험이 판정하려는 것이 이쪽이다.

grep -o '"[A-Za-z0-9_.$-]*":{"aliases":\[[^]]*\],"scope":"[a-z]*","type":"[^"]*"' /tmp/beans-after.json \
  | sed 's/{"aliases".*"type":"/ -> /' \
  | grep -i authorizedclient
  --- OAuth2 authorized client — 바뀌었는가? ---
    authorizedClientService
      before: InMemoryOAuth2AuthorizedClientService
      after : InMemoryOAuth2AuthorizedClientService    그대로 — Redis 로 안 옮겨졌다
    authorizedClientRepository
      before: AuthenticatedPrincipalOAuth2AuthorizedClientRepository
      after : AuthenticatedPrincipalOAuth2AuthorizedClientRepository    그대로 — Redis 로 안 옮겨졌다
    authorizedClientManager
      before: AuthorizedClientServiceOAuth2AuthorizedClientManager
      after : AuthorizedClientServiceOAuth2AuthorizedClientManager    그대로 — Redis 로 안 옮겨졌다

이 값이 뜻하는 것 — 빈 81개가 늘었는데 인가된 클라이언트는 하나도 안 바뀌었다.

   Application Session      ──▶ Redis        (인증 상태, principal, 인가 요청)
   OAuth2AuthorizedClient   ──▶ 프로세스 메모리  (access token, refresh token)

spring.session.store-typeHttpSession 을 갈아끼우는 설정이고 OAuth2AuthorizedClient 는 그 설정과 무관한 다른 저장소에 있다. 찍어서 확인하지 않으면 이 사실을 알 방법이 없다. 로그인은 되고 화면도 뜨고 파드도 건강하다. B-0 을 실험으로 만든 까닭이 여기서 드러난다 — before 가 있어야 after 를 읽는다.

2. Redis 를 직접 연다

무엇을 보는가 — 옮겨진 것 안에 무엇이 들었는지.

kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan

실측은 이렇다(observed, 03-redis-contents.txt).

=== Redis 에 무엇이 들어 있는가 ===
bff:session:sessions:8963b6de-3564-4775-9ccd-1ee9616b83ae
  총 키 수: 1

네임스페이스가 bff:session 이다. application.ymlspring.session.redis.namespace 가 그대로 접두어가 됐다.

키 이름을 변수로 잡는다. 이 줄에는 걸러 내는 조각이 둘 붙어 있다.

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"

grep -v expires 가 필요한 까닭은 Spring Session 이 만료 추적용 키인 bff:session:expirations:*bff:session:sessions:expires:* 도 만들기 때문이다. 그것을 잡으면 다음 명령이 빈 결과를 낸다. tr -d '\r'redis-cli 출력이 CR 을 달고 오기 때문에 붙인다. 빼면 키가 안 맞는데 오류는 안 난다.

바로 위 redis-cli --scan 이 이미 전체 키를 보여 줬으므로 그 출력에서 키 하나를 눈으로 골라 쳐도 되는데, 원 가이드가 그 두 단계 형태를 적어 두지 않았다(unknown).

head -1 이 엉뚱한 세션을 집는다(2026-09-17, observed). 원래 실행은 키가 하나였지만 둘 이상일 때가 있다. 2026-09-17 에 로그인 한 번 뒤 키가 둘이었고, head -1 이 고른 쪽에는 SPRING_SECURITY_CONTEXT 가 없었다.

bff:session:sessions:b66634a2-…    sessionAttr:SPRING_SECURITY_SAVED_REQUEST · maxInactiveInterval · creationTime · lastAccessedTime
bff:session:sessions:c5f7b0c7-…    sessionAttr:SPRING_SECURITY_CONTEXT · …AUTHORIZATION_REQUEST · SPRING_SECURITY_LAST_EXCEPTION · lastAccessedTime · maxInactiveInterval · creationTime

앞엣것은 로그인 화면으로 보내기 전에 만들어진 세션이고 인증 정보가 없다. 그것을 잡으면 아래 ③ 의 필드 목록에 SPRING_SECURITY_CONTEXT 가 안 나오고, 관찰 3 의 \xac\xed 도 못 본다 — 이 편이 보여 주려는 것이 통째로 안 보인다. 오류는 안 나므로 「토큰이 없네」가 아니라 「인증 세션이 아니네」를 먼저 의심해야 하는데, 화면만 봐서는 갈리지 않는다.

키를 내용으로 고르면 몇 개가 있든 맞는 것을 잡는다.

KEY=
for K in $(kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan --pattern 'bff:session:sessions:*' | grep -v expires | tr -d '\r'); do
  HAS=$(kubectl -n keycloak-lab exec deploy/redis -- redis-cli hexists "$K" 'sessionAttr:SPRING_SECURITY_CONTEXT' | tr -d '\r')
  echo "$HAS  $K"
  if [ "$HAS" = 1 ]; then KEY=$K; fi
done
echo "고른 키: $KEY"

첫 줄의 KEY= 를 뺀 채로 치면 이 절이 통째 무의미해진다. ② 가 이미 $KEY 를 채워 뇌기 때문에, 루프가 한 건도 못 맞혀도 마지막 echo② 가 잘못 고른 그 키를 그대로 찍는다. 화면은 정상으로 보이는데 고쳐진 것이 없다. 비우고 시작해야 「못 찾았다」 가 빈 줄로 보인다.

키마다 10 을 앞에 찍는 것도 같은 까닭이다. 걸러 내는 일을 grep -q 에 맡기면 어느 키가 왜 떨어졌는지가 화면에 안 남는다.

1  bff:session:sessions:0daf4915-a8a6-4b8e-bc97-5e58627eae23
1  bff:session:sessions:1e719069-aae5-47ee-a40b-0c70a0aae688
고른 키: bff:session:sessions:1e719069-aae5-47ee-a40b-0c70a0aae688

둘 다 1 이면 루프는 마지막 것을 잡는다. 어느 쪽을 잡아도 SPRING_SECURITY_CONTEXT 는 있으니 이 날은 상관이 없었지만, 「맞는 키가 하나뿐」 을 전제하고 읽으면 안 된다.

kubectl -n keycloak-lab exec deploy/redis -- redis-cli type "$KEY"
kubectl -n keycloak-lab exec deploy/redis -- redis-cli hkeys "$KEY"

어디를 보나 — 실측은 이렇다(observed, 03-redis-contents.txt).

  타입: hash
    필드: sessionAttr:SPRING_SECURITY_CONTEXT
    필드: sessionAttr:SPRING_SECURITY_SAVED_REQUEST
    필드: sessionAttr:SPRING_SECURITY_LAST_EXCEPTION
    필드: sessionAttr:org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository.AUTHORIZATION_REQUEST
    필드: lastAccessedTime
    필드: maxInactiveInterval
    필드: creationTime

2026-09-17 에 다시 치니 필드가 여섯이었다(observed) — sessionAttr:SPRING_SECURITY_SAVED_REQUEST 하나가 없었고 나머지 여섯은 같았다. 로그인 전에 뭐를 요청했는지에 따라 붙었다 말았다 하는 칸이라, 이 절이 보려는 것—토큰이 없다—은 그대로다.

이 값이 뜻하는 것 — 필드 목록에 토큰이 없다. 저장소를 직접 열어 refresh token 이 평문으로 남는지 확인하는 것이 검증 항목이었는데, 답은 더 앞에 있었다 — 애초에 들어가지 않는다. 토큰 암호화를 어떻게 할지 고민하기 전에 토큰이 그 저장소에 가지도 않는다는 것을 먼저 알아야 한다.

3. TTL 과 값의 바이트를 본다

kubectl -n keycloak-lab exec deploy/redis -- redis-cli ttl "$KEY"
=== TTL (Q3 검증 3번 — session TTL) ===
  TTL: 1772 초

spring.session.timeout=30m 인 1800초에서 방금 지난 만큼 줄어든 값이다. 세션 TTL 1772초와 access token 수명 60초가 처음부터 어긋나 있다. 어느 쪽에 맞출지 고르기 전에 이미 어긋나 있고, 그 간극을 누가 메우는지가 B-3 의 주제다.

kubectl -n keycloak-lab exec deploy/redis -- redis-cli --no-raw hgetall "$KEY" | head -4
 1) "sessionAttr:SPRING_SECURITY_CONTEXT"
 2) "\xac\xed\x00\x05sr\x00=org.springframework.security.core.context.SecurityContextImpl\x00\x00\x00\x00\x00\x00\x02l\x02\x00\x01L\x00\x0eauthenticationt\x002Lorg/springframework/security/core/Authentication;xpsr\x00Sorg.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken...

★ 필드 순서를 기대하면 안 된다(2026-09-17, observed). 위 head -4SPRING_SECURITY_CONTEXT 가 첫 필드로 나온다고 보고 자른 것인데, Redis 해시의 필드 순서는 보장되지 않는다. 같은 명령을 다시 쳤을 때는 이렇게 나왔다.

 1) "lastAccessedTime"
 2) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number…"

그래도 요점은 더 세게 확인된다. \xac\xedSPRING_SECURITY_CONTEXT 에만 붙는 것이 아니라 lastAccessedTimeLong 하나에도 붙는다. 이 해시의 값은 전부 Java 직렬화다. 특정 필드를 보려면 head 로 자르지 말고 hget "$KEY" 'sessionAttr:SPRING_SECURITY_CONTEXT' 로 이름을 대서 꺼낸다.

\xac\xed 로 시작한다. Java 직렬화 매직 넘버이고 JSON 이 아니다. --no-raw 를 쓰는 것은 바이너리를 이스케이프해서 보여 주기 때문이다. 안 쓰면 터미널이 제어문자를 먹고 화면이 깨진다.

결과
사람이 못 읽는다 운영 중 디버깅이 어렵다
클래스 버전에 묶인다 애플리케이션을 올리면 기존 세션이 역직렬화에 실패할 수 있다
역직렬화 취약점 신뢰할 수 없는 데이터가 들어오면 위험한 형식이다

D-2 의 버전 업그레이드에서 이 성질이 다시 나온다. Spring Security 버전이 바뀌면 Redis 에 있던 세션이 깨질 수 있다.

4. 파드를 전부 교체하고 사용자 화면을 본다

목적 — 롤링 재시작으로 Redis 덕을 보는지 확인한다. 롤링 재시작은 정상 작업이라 되돌릴 것이 없다.

kubectl -n keycloak-lab rollout restart deployment/bff
kubectl -n keycloak-lab rollout status deployment/bff --timeout=300s

로그인은 그대로 둔 채 브라우저에서 https://app1.hyeonworks.com/bff/token-boundary 를 연다.

예상 결과 — 실측은 이렇다(observed, b1-token-boundary-after-redis.png).

{"pattern":"AP3-backend-for-frontend",
 "principal":"labuser",                   세션은 Redis 에서 복원되었다
 "accessTokenStoredOnServer":false,       토큰은 사라졌다
 "refreshTokenStoredOnServer":false,
 "browserTokenCount":0,
 "csrfProtectionEnabled":true}

왜 필요한가principal 은 살아 있는데 두 토큰이 false 다.

   사용자 관점:  로그인되어 있다고 나온다
   실제:        BFF 가 사용자를 대신해 아무것도 못 한다

파드가 전부 교체됐는데 로그인 상태는 살아남았다. Redis 덕분이다. 토큰은 같이 살아남지 못했다. 인스턴스 메모리에 있었으니까. 원 가이드는 이것을 부분적으로만 공유했을 때의 실패 모양이라고 부르고, 완전히 로그아웃되는 편이 차라리 낫다고 적는다. 적어도 사용자가 다시 로그인하는데, 지금은 화면상 로그인 상태라 사용자가 아무것도 안 한다.

B-0 (Redis 없음, replica 1) B-1 (Redis 세션, replica 2)
principal labuser labuser
accessTokenStoredOnServer true false
파드 재시작 후 로그아웃 로그인 상태만 남고 토큰은 소실

문제가 생기면 — 스크린샷으로 시점을 구별하지 않는다. 증거 폴더의 README.md 가 적어 둔 대로 b1-login-works-two-replicas.pngb1-token-boundary-after-redis.png 는 동일 파일이다. 세 시점 모두 accessTokenStoredOnServer: false 인 같은 화면이었고, 시점 구별은 터미널 출력과 Redis·DB 조회가 한다.

그래서 무엇을 해야 하는가 — OAuth2AuthorizedClientService 를 공유 저장소로 옮기는 구현이 따로 필요하다.

후보
JdbcOAuth2AuthorizedClientService Spring Security 기본 제공. PostgreSQL 이 이미 있다
직접 구현 (Redis) OAuth2AuthorizedClientService 인터페이스를 Redis 로 구현
세션 안에 넣기 HttpSessionOAuth2AuthorizedClientRepository 를 쓰면 세션과 함께 Redis 로 간다

세 번째는 조회 키 문제까지 같이 푼다. 세션 단위로 저장되므로 같은 사용자의 다른 브라우저가 서로를 덮어쓰지 않고, 대신 세션이 커진다. B-2 가 이 선택지를 비교한다.

복구와 원상복구 확인표

B-2 로 이어갈 것이면 이 구성이 B-2 의 출발이므로 아무것도 안 되돌린다.

1. 소스를 되돌리고 다시 빌드해 다시 밀어 넣는다

B-0 상태로 되돌릴 때는 소스만 되돌려서는 안 된다. 클러스터에는 여전히 옛 이미지가 돈다.

git checkout -- bff/pom.xml bff/src/main/resources/application.yml \
  bff/src/test/java/com/example/keycloakpattern/bff/BffControllerTest.java \
  deploy/lab/k8s/bff-redis.yaml
git status --short
docker build --progress=plain -t keycloak-pattern-bff:lab bff/ > /tmp/build.log 2>&1
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 -'"
kubectl apply -f deploy/lab/k8s/bff-redis.yaml
kubectl -n keycloak-lab rollout restart deployment/bff
kubectl -n keycloak-lab rollout status deployment/bff --timeout=300s

② 도 한 블록에 기계가 둘이다. docker 로 시작하는 앞의 세 줄은 워크스테이션, kubectl 로 시작하는 뒤의 세 줄은 kc-lab-1 이다. ① 이 되돌린 파일이 kc-lab-1 쪽 체크아웃에도 반영돼 있어야 apply 가 B-0 구성을 올린다.

git status --short 가 빈 출력이어도 클러스터는 아직 옛 이미지를 쓰고 있다. ② 를 끝내야 소스와 클러스터가 같아진다.

2. Redis 를 비운다

Redis 를 비우는 것은 되돌릴 수 없다. 지운 세션은 돌아오지 않고 로그인한 사용자는 전부 로그아웃된다. 실험대라서 하는 일이다.

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

세션 하나만 지우려면 이쪽이다. $KEY 는 관찰 2번에서 담아 둔 키이고, 그 뒤로 rollout restart 와 브라우저 왕복이 들어가 절차가 40분쯤 걸리므로 터미널을 새로 열었으면 값이 비어 있다. 치기 전에 echo "$KEY" 로 키가 나오는지 보고, 안 나오면 관찰 2번의 KEY=$(...) 두 줄을 다시 쳐서 담는다. 바로 위 ① 의 flushdb 를 이미 쳤으면 지울 키가 없으므로 ② 를 건너뛴다.

kubectl -n keycloak-lab exec deploy/redis -- redis-cli del "$KEY"
항목 명령 돌아왔을 때
소스 git status --short 출력 없음 (B-0 로 되돌릴 때)
이미지 sudo k3s ctr images ls | grep keycloak-pattern-bff 두 노드 모두에 있다
파드 kubectl -n keycloak-lab get pods -o wide -l app=bff 2/2, 두 노드에 하나씩
Redis kubectl -n keycloak-lab exec deploy/redis -- redis-cli ping PONG
Redis 키 ... redis-cli dbsize 의도한 값
Keycloak kubectl -n keycloak-lab get pods | grep keycloak 둘 다 1/1 Running
curl -I https://app1.hyeonworks.com/ 200
임시 파일 rm -f /tmp/beans-before.json /tmp/beans-after.json /tmp/build.log

표의 두 줄은 한 기계에서 다 못 본다. 이미지 줄의 sudo k3s ctr images ls 는 그 명령을 친 노드 하나만 본다. kc-lab-1 에서 치면 kc-lab-2 는 안 보이므로, 「두 노드 모두에 있다」를 확인하려면 B-0 이 짝으로 친 ssh kc-lab-2 'sudo k3s ctr images ls | grep keycloak-pattern-bff' 도 함께 봐야 한다. 한쪽에만 있으면 그 노드의 replica 만 뜨고 나머지는 ErrImageNeverPull 로 나타난다. 임시 파일 줄도 앞의 두 파일은 kc-lab-1, /tmp/build.log 는 워크스테이션에 있으므로 각 기계에서 자기 쪽을 지운다.

막히면

원 가이드는 이 표를 두고 전부 이 실험대가 실제로 겪은 증상이고 지어낸 것은 없다고 적는다.

증상 원인 확인
파드가 CrashLoopBackOff 이고 오류에 tcp://...:6379 쿠버네티스가 REDIS_PORT 를 주입했다 printenv | grep -i redis
위 오류를 Redis 가 죽어서로 읽었다 메시지가 Failed to bind properties redis-cli ping 으로 Redis 를 따로 확인
enableServiceLinks 를 넣었는데 그대로다 파드가 아직 옛 것이다 rollout restartprintenv 다시
빌드가 Redis 연결 오류로 죽는다 테스트가 Redis 를 찾는다 spring.session.store-type=none
sessionRepository 가 안 생긴다 의존성을 하나만 넣었다. 오류 없이 메모리에 남는다 두 개 다 있는지 pom.xml
redis-cli --scan 이 비어 있다 아직 로그인 안 했다 브라우저로 로그인 후 다시
hkeys 가 빈 결과 만료 추적 키를 잡았다 grep -v expires
키가 맞는데 명령이 안 먹는다 출력에 CR 이 붙었다 tr -d '\r'
값이 깨져서 터미널이 이상해진다 바이너리를 그대로 찍었다 --no-raw
API 호출이 500 인데 토큰은 멀쩡하다 DNS 다. 다른 네임스페이스의 서비스 로그의 UnresolvedAddressException
/actuator/beansBad Gateway 응답이 커서 프록시가 못 넘긴다 파드 안에서 받는다
jq: command not found 이 실험대에 jq 가 없다 grep 으로 읽는다
로그인은 되는데 API 가 전부 실패한다 이게 이 실험의 결론이다 token-boundary 의 두 false
스크린샷으로 시점을 구별하려다 헷갈린다 두 파일이 동일하다 터미널 출력과 Redis 조회로 구별

500 쪽은 원인을 찾는 데 한 번 헛짚었다. 로그를 보니 토큰이 아니라 DNS 였다.

아래 줄의 $BFF 는 주입 검증 1번에서 담아 둔 파드 이름이다. 「막히면」은 아무 때나 펼치는 절이고 그 사이에 관찰 4번의 rollout restart 가 파드를 통째로 갈아치우므로, 그대로 치면 NotFound 가 온다. 치기 전에 주입 검증 1번의 BFF=$(...) 두 줄을 다시 쳐서 이름을 새로 담는다.

kubectl -n keycloak-lab logs "$BFF" --tail=100 | grep -iE 'exception|error'
java.nio.channels.UnresolvedAddressException

RESOURCE_API_BASE_URL=http://echo.keycloak-lab.svc:8080 이었는데 echoheader-lab 네임스페이스의 8081 이었다. 배포조차 되어 있지 않았다.

# 다른 네임스페이스의 서비스는 <svc>.<ns>.svc 로 부른다
- name: RESOURCE_API_BASE_URL
  value: http://echo.header-lab.svc:8081
kubectl -n header-lab get svc echo

이 실험대에서 아직 못 밟은 단계

2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. 못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.

  • 끝까지 밟았다(2026-09-17). TLS 가 선 뒤 로그인해서 Redis 키·필드·TTL 까지 봤다. 로그인은 브라우저 대신 쿠키 항아리를 쓴 curl 로 인가 코드 흐름을 돌렸고, 왕복 두 번과 쿠키가 그대로 재현된다. replica 2 에서 로그인이 됐고(B-0 은 여기서 실패한다) bff/token-boundaryaccessTokenStoredOnServer=true · browserTokenCount=0 을 냈다. 브라우저 쪽 쿠키는 AP3_SESSION 48자 하나뿐이다.
  • 막는 것https://auth.hyeonworks.com 이 서지 않는다. 남은 것은 인증서 하나다(2026-09-17 기준). 같이 적어 두었던 다른 둘은 그날 해결됐다 — 밖에서 닿는 길은 호스트의 libvirt guest_input 구멍과 유닛의 ExecStartPost 가 들어가면서 열렸고(http 가 밖에서 200), 클러스터 안에서 그 이름이 엣지를 안 가리키던 것은 기반 가이드 03 의 CoreDNS 한 단계로 놓았다. 인증서는 Cloudflare API 토큰이 필요한 DNS-01 로만 받을 수 있다 — 이름 셋이 tailnet 주소로 풀려 HTTP-01 은 성립하지 않는다.
  • 그때까지 이 편의 실측 가운데 (observed) 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다. 나머지는 원래 실행의 값이다.

무엇이 관측이고 무엇이 아닌가

이 절차의 숫자는 2026-09-04 13:5914:03 KST 에 돈 한 번의 실행에서 나왔다(observed).

  • (observed) 자동 주입된 REDIS_* 일곱 줄과 REDIS_PORT=tcp://10.43.57.116:6379, Failed to bind properties 오류 전문, enableServiceLinks: false 뒤의 롤아웃 출력 세 줄, 빈 수 321 → 402 (+81), 새로 생긴 세션 저장소 빈 여섯 줄, 안 바뀐 인가된 클라이언트 빈 세 개의 before 와 after, Redis 키 bff:session:sessions:8963b6de-3564-4775-9ccd-1ee9616b83ae 와 필드 일곱 개, TTL: 1772 초, \xac\xed 로 시작하는 바이트, 재시작 뒤 token-boundaryprincipal 생존과 두 토큰 false, UnresolvedAddressException.
  • (unknown) 빈을 세는 grep -o '"aliases":\[' 줄과 이름·타입을 뽑는 grep·sed 줄. 원 가이드가 미검증으로 표시했다. jq 판본과, --scan 출력에서 키를 눈으로 골라 치는 두 단계 형태와, 두 겹 ssh 를 나눠 치는 형태도 가이드에 없다.
  • (observed) b1-login-works-two-replicas.pngb1-token-boundary-after-redis.png 가 동일 파일이라는 것은 증거 폴더의 README.md 가 적어 둔 사실이다. 같은 화면이 세 시점에 나왔기 때문이고, 그래서 시점은 터미널 출력과 저장소 조회로만 갈린다.
  • 이 실험이 재지 않은 것 셋 — Redis 를 끊었을 때 무엇이 나는지는 B-5 의 주제이고, 로그아웃 뒤 두 저장소에 무엇이 있는지는 B-2 로 넘겼으며, 저장소 지연이 화면 지연으로 얼마나 번역되는지는 B-2 이후에 잰다.