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>
47 KiB
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 |
|
|
cdac9b8178391311d8eca1ebc6cac15bb62d79af |
Redis 를 붙이고 무엇이 옮겨졌는지 빈 목록으로 견준다
Redis 를 세션 저장소로 붙이고 B-0 에서 찍어 둔 빈 목록과 견주는 절차다. 의존성 둘을 함께 넣어 다시 빌드하고 두 노드에 밀어 넣은 뒤, 무엇이 옮겨졌는지와 무엇이 안 옮겨졌는지를 같은 명령으로 확인한다. 전 구간 약 40분이고 빌드 시간이 들어 있다.
관계
- 세션만 Redis 로 옮기자 토큰이 따라오지 않았다 이 절차가 낸 결과를 담은 기록이다. 무엇을 발견했는지는 그쪽에 있고 여기에는 치는 순서만 있다.
- 주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다 의존성을 하나만 넣으면 오류 없이 메모리에 남는다. 그 아홉 건 가운데 하나가 이 편에서 나왔다.
- 세션과 인가된 클라이언트는 조회 키가 다르다 빈 81개가 늘었는데 인가된 클라이언트 빈 셋이 그대로인 까닭을 그 기록이 설명한다.
- 아무 저장소도 주지 않고 Spring 이 무엇을 고르는지 찍어서 확인한다
먼저 해 둬야 하는 편이다. 그 편이 남긴 빈 세 개의 이름과 숫자
321이 여기서 대조군이 된다. - 토큰을 PostgreSQL 로 옮기고 기본키와 로그아웃 정리를 확인한다 다음 편이다. 여기서 Redis 로 안 옮겨진 인가된 클라이언트를 그 편이 공유 저장소로 옮긴다.
본문
읽기 전에 — 어디서 치는가
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).
첫째, 워크스테이션과 kc-lab-1 을 여섯 번 오가는데 건너가는 ssh 도 exit 도 이 절차에 없다. 경로는 이미지를 밀어 넣는 줄 하나에만 드러난다 — ssh test-server "ssh kc-lab-1 '...'".
둘째, bff/pom.xml 과 deploy/lab/k8s/bff-redis.yaml 이 전부 저장소 상대경로다. 두 기계 각각에서 저장소 루트로 옮겨 두고 시작한다.
셋째, 워크스테이션에서 고친 deploy/lab/k8s/bff-redis.yaml 을 kc-lab-1 로 넘기는 단계가 없다. 이 편에서는 그 누락이 B-0 보다 더 헷갈리게 나온다 — 주입 3번이 넣는 enableServiceLinks: false 가 kc-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 가 이 실험대에 없다. grep 과 redis-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 「전부 지운다」를 치지 않은 상태여야 한다. 그 절은
bff와redisDeployment 와 realmkeycloak-patterns를 함께 없애므로, 쳤으면 아래 첫 확인부터 빈 목록이 나온다. 그 상태라면 B-0 의 주입 절과 realm·사용자 단계를 다시 밟아 배포를 세운 뒤 여기로 온다. - 브라우저가 필요하다.
- 명령은
kc-lab-1에서 친다. jq는 이 실험대에 깔려 있지 않다. 이 절차는grep과redis-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" 를 명시하면 그쪽이 이긴다. 명시를 안 하면 자동 주입이 이기고, 오류 메시지는 쓰지도 않은 값을 지목한다. REDIS 와 POSTGRES 와 MYSQL 처럼 흔한 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_PORT 가 6379 다.
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-type 은 HttpSession 을 갈아끼우는 설정이고 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.yml 의 spring.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 는 ② 가 잘못 고른 그 키를 그대로 찍는다. 화면은 정상으로 보이는데 고쳐진 것이 없다. 비우고 시작해야 「못 찾았다」 가 빈 줄로 보인다.
키마다 1 과 0 을 앞에 찍는 것도 같은 까닭이다. 걸러 내는 일을 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 -4 는 SPRING_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\xed 가 SPRING_SECURITY_CONTEXT 에만 붙는 것이 아니라 lastAccessedTime 의 Long 하나에도 붙는다. 이 해시의 값은 전부 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.png 와 b1-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 restart 후 printenv 다시 |
| 빌드가 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/beans 가 Bad 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 이었는데 echo 는 header-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-boundary가accessTokenStoredOnServer=true·browserTokenCount=0을 냈다. 브라우저 쪽 쿠키는AP3_SESSION48자 하나뿐이다. - 막는 것 —
https://auth.hyeonworks.com이 서지 않는다. 남은 것은 인증서 하나다(2026-09-17 기준). 같이 적어 두었던 다른 둘은 그날 해결됐다 — 밖에서 닿는 길은 호스트의 libvirtguest_input구멍과 유닛의ExecStartPost가 들어가면서 열렸고(http가 밖에서200), 클러스터 안에서 그 이름이 엣지를 안 가리키던 것은 기반 가이드 03 의 CoreDNS 한 단계로 놓았다. 인증서는 Cloudflare API 토큰이 필요한 DNS-01 로만 받을 수 있다 — 이름 셋이 tailnet 주소로 풀려 HTTP-01 은 성립하지 않는다. - 그때까지 이 편의 실측 가운데
(observed)로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다. 나머지는 원래 실행의 값이다.
무엇이 관측이고 무엇이 아닌가
이 절차의 숫자는 2026-09-04 13:59–14: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-boundary의principal생존과 두 토큰false,UnresolvedAddressException. - (unknown) 빈을 세는
grep -o '"aliases":\['줄과 이름·타입을 뽑는grep·sed줄. 원 가이드가 미검증으로 표시했다.jq판본과,--scan출력에서 키를 눈으로 골라 치는 두 단계 형태와, 두 겹ssh를 나눠 치는 형태도 가이드에 없다. - (observed)
b1-login-works-two-replicas.png와b1-token-boundary-after-redis.png가 동일 파일이라는 것은 증거 폴더의README.md가 적어 둔 사실이다. 같은 화면이 세 시점에 나왔기 때문이고, 그래서 시점은 터미널 출력과 저장소 조회로만 갈린다. - 이 실험이 재지 않은 것 셋 — Redis 를 끊었을 때 무엇이 나는지는 B-5 의 주제이고, 로그아웃 뒤 두 저장소에 무엇이 있는지는 B-2 로 넘겼으며, 저장소 지연이 화면 지연으로 얼마나 번역되는지는 B-2 이후에 잰다.