diff --git a/docs/keycloak-session-store/final/document.md b/docs/keycloak-session-store/final/document.md index 34b617e..34f1600 100644 --- a/docs/keycloak-session-store/final/document.md +++ b/docs/keycloak-session-store/final/document.md @@ -13729,8 +13729,8 @@ code=200 size=155395 설정이 바뀌면 다시 막힐 수 있다. 다만 `Bad Gateway` 를 「이 주소는 원래 밖에서 안 된다」로 읽지는 않는다. -**B-0 의 주입은 2026-09-17 에 1 절(파일 넷 편집)과 2 절의 빌드까지 밟았다** -(observed). 두 노드에 이미지를 밀어 넣는 단계부터는 안 밟았다(unknown). +**B-0 의 주입은 2026-09-17 에 끝까지 다시 밟았다**(observed) — 파일 넷 편집·빌드부터 +배포·관찰·원상복구까지. **★ ⑤ 의 `git diff --stat` 점검은 작업 트리가 깨끗할 때만 성립한다**(2026-09-17, observed). 그 줄은 「네 파일만 나오는가」로 덜 지운 것을 잡으려는 것인데, @@ -13762,6 +13762,41 @@ exit=0 replica 2 로 로그인하면 **성공한다.** B-0 은 같은 조건에서 실패한다고 적으므로, 두 편을 가르는 것이 세션 저장소 하나임이 반대편에서 확인된다. +자동구성이 고른 것은 이렇다(observed). 빈이 321개로 이 편이 예측한 숫자와 같고, +`SessionRepository` 빈은 하나도 없다. + +```text +빈 개수: 321 +authorizedClientService -> org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService +OAuth2AuthorizedClientManager -> org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager +authorizedClientRepository -> org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository +SessionRepository 계열 -> 없다 +타입에 Redis 나 Jdbc 가 든 빈 -> 없다 +``` + +로그인은 replica 개수에서 갈렸다. 쿠키 병을 쓰는 `curl` 로 같은 흐름을 여섯 번씩 +돌려 양쪽을 같은 수로 견켜 봤다. + +```text +replica 2 — 여섯 번 다 https://app1.hyeonworks.com/login?error 200 +replica 1 — 여섯 번 다 https://app1.hyeonworks.com/ 200 +``` + +한 번만 치면 replica 2 에서 우연히 같은 파드로 가 성공할 수도 있고, 그러면 반대로 +읽힌다. replica 1 쪽은 대조군이다 — 거기서도 실패했으면 가설이 아니라 시험 +방법을 의심해야 한다. 토큰 경계는 원래 실행과 한 글자도 다르지 않았다. + +원상복구도 같은 명령으로 확인했다. + +| 재본 값 | B-0 | 복구 뒤 | +|---|---|---| +| 빈 개수 | `321` | `437` | +| `authorizedClientService` | `InMemoryOAuth2AuthorizedClientService` | `JdbcOAuth2AuthorizedClientService` | +| 타입에 Redis 나 Jdbc 가 든 빈 | `0` | `58` | +| replica 2 로그인 6회 | 6번 다 `/login?error` | 6번 다 `200` | +| Redis 세션 키 | — | `6` | + + 그래서 파드 안에서 직접 받는다. **alpine 기반 JRE 이미지에는 `wget` 이 있다** — Keycloak 이미지와 다른 점이다. diff --git a/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/README.txt b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/README.txt index c793c1a..14369f7 100644 --- a/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/README.txt +++ b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/README.txt @@ -265,3 +265,13 @@ d4-01-before-force-renewal.txt / d4-02-force-renewal.txt / d4-03-after-force-ren 03 = 친 뒤의 같은 세 가지 + 세 이름의 HTTPS 응답. serial 06F3E0EF…1373 → 065547991777…3DF1, notAfter Dec 3 → Dec 16, worker 2629 4712 → 4745 4754. 갱신이 서빙까지 닿았다는 근거. +b0-04-image-push.txt … b0-12-revert-checklist.txt + B-0 을 2026-09-17 에 주입부터 원상복구까지 끝까지 밟은 기록. + 04 = 두 노드에 이미지 import · 05 = 배포와 롤아웃 + 06·07 = 파드 안에서 받은 빈 목록과 자동구성이 고른 구현 + (빈 321개 · InMemoryOAuth2AuthorizedClientService · SessionRepository 없음) + 08 = replica 2 에서 로그인 6회 — 전부 /login?error + 09 = replica 1 로 줄여 같은 시험 6회 — 전부 200 (대조군) + 10 = 로그인 뒤 /bff/token-boundary + 11·12 = 이미지와 매니페스트를 되돌리고 replica 2 에서 로그인이 다시 되는 것까지 확인 + (빈 437개 · JdbcOAuth2AuthorizedClientService · Redis 세션 키 6) diff --git a/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-04-image-push.txt b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-04-image-push.txt new file mode 100644 index 0000000..8328762 --- /dev/null +++ b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-04-image-push.txt @@ -0,0 +1,3 @@ +=== 4. 두 노드의 이미지 목록 === +docker.io/library/keycloak-pattern-bff:lab application/vnd.oci.image.index.v1+json sha256:ef0bd8559b22fe81bc98ebf6462cfef7f42706586528b15a31e86f10b45acbe4 96.5 MiB linux/amd64 io.cri-containerd.image=managed +docker.io/library/keycloak-pattern-bff:lab application/vnd.oci.image.index.v1+json sha256:ef0bd8559b22fe81bc98ebf6462cfef7f42706586528b15a31e86f10b45acbe4 96.5 MiB linux/amd64 io.cri-containerd.image=managed diff --git a/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-05-deploy.txt b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-05-deploy.txt new file mode 100644 index 0000000..dfbd1a7 --- /dev/null +++ b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-05-deploy.txt @@ -0,0 +1,10 @@ +Waiting for deployment "bff" rollout to finish: 1 out of 2 new replicas have been updated... +Waiting for deployment "bff" rollout to finish: 1 out of 2 new replicas have been updated... +Waiting for deployment "bff" rollout to finish: 2 old replicas are pending termination... +Waiting for deployment "bff" rollout to finish: 1 old replicas are pending termination... +Waiting for deployment "bff" rollout to finish: 1 old replicas are pending termination... +Waiting for deployment "bff" rollout to finish: 1 old replicas are pending termination... +deployment "bff" successfully rolled out +bff-5d97897676-zvfhp 0/1 Error 0 4h48m 10.42.1.51 kc-lab-2 +bff-6d59c54797-2wnzn 1/1 Running 0 44s 10.42.0.40 kc-lab-1 +bff-6d59c54797-72cch 1/1 Running 0 21s 10.42.1.94 kc-lab-2 diff --git a/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-06-beans-after-injection.txt b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-06-beans-after-injection.txt new file mode 100644 index 0000000..2f6e540 --- /dev/null +++ b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-06-beans-after-injection.txt @@ -0,0 +1,5 @@ +=== 관찰 1 — 파드 안에서 빈 목록을 받는다 === +BFF=bff-6d59c54797-2wnzn +117450 /tmp/beans-b0.json +--- 저장소 계열 빈 --- +OAuth2AuthorizedClientManager diff --git a/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-07-what-autoconfig-picked.txt b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-07-what-autoconfig-picked.txt new file mode 100644 index 0000000..795d210 --- /dev/null +++ b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-07-what-autoconfig-picked.txt @@ -0,0 +1,26 @@ +=== 관찰 2 — 빈을 세고 저장소 관련 이름을 뽑는다 === +--- 빈 개수 --- +321 +--- 세션 저장소로 무엇을 골랐나 --- +--- authorized client service 는 무엇인가 --- +"aliases":[],"scope":"singleton","type":"org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository","resource":"class path resource [org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2ClientWebSecurityAutoConfiguration.class]","dependencies":["org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientWebSecurityAutoConfiguration","authorizedClientService"]},"enableGlobalAuthenticationAutowiredConfigurer": +"aliases":[],"scope":"singleton","type":"org.springframework.web.util.UrlPathHelper","resource":"class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]","dependencies":["org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"]},"org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConfigurations$OAuth2AuthorizedClientServiceConfiguration": +"aliases":[],"scope":"singleton","type":"org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConfigurations$OAuth2AuthorizedClientServiceConfiguration","dependencies":[]},"org.springframework.boot.actuate.autoconfigure.metrics.startup.StartupTimeMetricsListenerAutoConfiguration": +"aliases":[],"scope":"singleton","type":"com.example.keycloakpattern.bff.BffController","resource":"URL [jar:nested:/app/app.jar/!BOOT-INF/classes/!/com/example/keycloakpattern/bff/BffController.class]","dependencies":["authorizedClientService","OAuth2AuthorizedClientManager","restClientBuilder"]},"org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedTomcat": +"aliases":[],"scope":"singleton","type":"org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder","resource":"class path resource [org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations$ThreadPoolTaskSchedulerBuilderConfiguration.class]","dependencies":["org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$ThreadPoolTaskSchedulerBuilderConfiguration","spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties"]},"authorizedClientService": +--- Redis / Jdbc 흔적이 남았나 --- +0 +0 +=== authorizedClientService 의 실제 구현 === + File "", line 7 + if n in beans: print(f"{n}: {beans[n][\"type\"]}") + ^ +SyntaxError: unexpected character after line continuation character +빈 개수: 321 +authorizedClientService -> org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService +OAuth2AuthorizedClientManager -> org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager +authorizedClientRepository -> org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository +--- SessionRepository 계열 --- +없다 — Spring Session 이 안 걸렸다 +--- 타입에 Redis 나 Jdbc 가 든 빈 --- +없다 diff --git a/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-08-login-replica2.txt b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-08-login-replica2.txt new file mode 100644 index 0000000..5c19bd1 --- /dev/null +++ b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-08-login-replica2.txt @@ -0,0 +1,6 @@ +1 https://app1.hyeonworks.com/login?error 200 +2 https://app1.hyeonworks.com/login?error 200 +3 https://app1.hyeonworks.com/login?error 200 +4 https://app1.hyeonworks.com/login?error 200 +5 https://app1.hyeonworks.com/login?error 200 +6 https://app1.hyeonworks.com/login?error 200 diff --git a/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-09-login-replica1.txt b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-09-login-replica1.txt new file mode 100644 index 0000000..c3817cf --- /dev/null +++ b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-09-login-replica1.txt @@ -0,0 +1,13 @@ +=== 4. replica 를 1 로 줄인다 === +deployment.apps/bff scaled +deployment "bff" successfully rolled out +bff-6d59c54797-2wnzn 1/1 Running 0 107s +bff-6d59c54797-72cch 1/1 Terminating 0 84s + +=== 같은 시험을 replica 1 에서 === +1 https://app1.hyeonworks.com/ 200 +2 https://app1.hyeonworks.com/ 200 +3 https://app1.hyeonworks.com/ 200 +4 https://app1.hyeonworks.com/ 200 +5 https://app1.hyeonworks.com/ 200 +6 https://app1.hyeonworks.com/ 200 diff --git a/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-10-token-boundary.txt b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-10-token-boundary.txt new file mode 100644 index 0000000..a9073fd --- /dev/null +++ b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-10-token-boundary.txt @@ -0,0 +1,3 @@ +로그인 뒤 주소: https://app1.hyeonworks.com/ +--- /bff/token-boundary --- +{"pattern":"AP3-backend-for-frontend","principal":"labuser","accessTokenStoredOnServer":true,"refreshTokenStoredOnServer":true,"browserTokenCount":0,"csrfProtectionEnabled":true} diff --git a/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-11-revert.txt b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-11-revert.txt new file mode 100644 index 0000000..7d0fe34 --- /dev/null +++ b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-11-revert.txt @@ -0,0 +1,23 @@ +=== 복구 1. replica 를 되돌린다 === +deployment.apps/bff scaled + +=== 복구 2. 소스(이미지)를 되돌린다 === +Importing elapsed: 6.3 s total: 0.0 B (0.0 B/s) +Importing elapsed: 5.5 s total: 0.0 B (0.0 B/s) +secret/bff-secrets configured +persistentvolumeclaim/redis-data unchanged +deployment.apps/redis unchanged +service/redis unchanged +deployment.apps/bff configured +service/bff unchanged +ingress.networking.k8s.io/bff unchanged +deployment.apps/bff restarted +Waiting for deployment "bff" rollout to finish: 1 out of 2 new replicas have been updated... +Waiting for deployment "bff" rollout to finish: 1 out of 2 new replicas have been updated... +Waiting for deployment "bff" rollout to finish: 1 out of 2 new replicas have been updated... +Waiting for deployment "bff" rollout to finish: 1 out of 2 new replicas have been updated... +Waiting for deployment "bff" rollout to finish: 1 out of 2 new replicas have been updated... +Waiting for deployment "bff" rollout to finish: 1 old replicas are pending termination... +Waiting for deployment "bff" rollout to finish: 1 old replicas are pending termination... +Waiting for deployment "bff" rollout to finish: 1 old replicas are pending termination... +deployment "bff" successfully rolled out diff --git a/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-12-revert-checklist.txt b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-12-revert-checklist.txt new file mode 100644 index 0000000..24d5725 --- /dev/null +++ b/docs/keycloak-session-store/final/evidence/raw/relive-2026-09-17/b0-12-revert-checklist.txt @@ -0,0 +1,18 @@ +=== 원상복구 확인표 === +--- 파드 --- +bff-6588776c5-fmv5g 1/1 Running 0 49s 10.42.1.96 kc-lab-2 +bff-6588776c5-j9ldm 1/1 Running 0 28s 10.42.0.41 kc-lab-1 +--- Redis 세션 키가 다시 생기나 (B-1 구성으로 돌아왔다는 뜻) --- +--- replica 2 에서 로그인이 다시 되는가 (복구 판정) --- +1 https://app1.hyeonworks.com/ 200 +2 https://app1.hyeonworks.com/ 200 +3 https://app1.hyeonworks.com/ 200 +4 https://app1.hyeonworks.com/ 200 +5 https://app1.hyeonworks.com/ 200 +6 https://app1.hyeonworks.com/ 200 +--- 로그인 여섯 번 뒤의 Redis 세션 키 --- +6 +--- 빈 목록이 B-1·B-2 로 돌아왔나 --- +빈 개수: 437 +authorizedClientService -> org.springframework.security.oauth2.client.JdbcOAuth2AuthorizedClientService +Redis/Jdbc 타입 빈: 58 diff --git a/docs/keycloak-session-store/tech-log-studio/tech-log-tree.json b/docs/keycloak-session-store/tech-log-studio/tech-log-tree.json index b4ba90d..86bcf42 100644 --- a/docs/keycloak-session-store/tech-log-studio/tech-log-tree.json +++ b/docs/keycloak-session-store/tech-log-studio/tech-log-tree.json @@ -7,7 +7,7 @@ "revision": "9465582b5d1630eb4ae7c4e078021486919bf6b6", "verified": "반입할 때 적어 둔 source/.source-revision 이 이 커밋이고 저장소에 실재한다 — 「chore: 실행 환경 구성 문서 추가 및 수정」, 2026-09-10. **전에 이 칸은 cdac9b81 이었고 그것은 틀렸다** (2026-09-17 대조) — 그 커밋은 2026-09-04 이고 반입본 306개 가운데 docs/guides/** 28개가 거기에 아예 없다. 가이드는 그 엿새 뒤 6f6ab86 에서 들어왔다. **그런데 반입한 바이트는 이 커밋과도 같지 않다** — 9465582b 와 같은 것은 276개이고 29개가 다르다. 같은 306개를 저장소의 **작업 트리**와 견주면 297개가 같다. HEAD 에서 200 커밋을 거슬러 전수 대조했을 때 가장 가까운 6f6ab86 도 28개가 어긋났다. **맞는 커밋은 없다** — 반입은 커밋이 아니라 **그 시점의 작업 트리**(미커밋 수정이 있던 상태)에서 떠 온 것이다. 지금도 저장소는 그 파일들을 M 으로 낸다. 작업 트리와 남은 차이 8개 가운데 5개가 그 M 목록에 있고(반입 뒤 저장소가 더 고쳤다), deploy/lab/host/nginx-keycloak-lab.conf 는 저장소에서 deploy/lab/edge/ 로 옮겨져 반입본에만 남았다. **이 커밋은 「반입 시점의 HEAD」라는 뜻이지 「반입한 바이트가 이것이다」가 아니다**" }, - "ssotSha256": "2ef1b420217e7a949917f13c6ffcb672516433b99d53dfd761f4f1f662653e55", + "ssotSha256": "02d7dcfb862178b4b0a49453bf8ef0693226d7ab52ca44d695d9b988b471c265", "sourceRevision": "keycloak-session-lab@2026-09", "generatedAt": "2026-09-17", "candidateScope": { diff --git a/docs/keycloak-session-store/tech-log-studio/where-application-state-lives/setup/setup-reproduce-b0-default-session-store.md b/docs/keycloak-session-store/tech-log-studio/where-application-state-lives/setup/setup-reproduce-b0-default-session-store.md index a1aaf2e..c996984 100644 --- a/docs/keycloak-session-store/tech-log-studio/where-application-state-lives/setup/setup-reproduce-b0-default-session-store.md +++ b/docs/keycloak-session-store/tech-log-studio/where-application-state-lives/setup/setup-reproduce-b0-default-session-store.md @@ -825,13 +825,11 @@ authorization-uri: ${KC_ISSUER_EXTERNAL}/protocol/openid-connect/auth authorization-uri: ${KC_ISSUER_EXTERNAL:http://localhost:8080/realms/keycloak-patterns}/protocol/openid-connect/auth ``` -## 이 실험대에서 어디까지 밟았나 +## 2026-09-17 에 끝까지 다시 밟았다 -2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈쮘는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.** +기반 가이드로 실험대를 새로 세우고 이 편을 주입부터 원상복구까지 다시 밟았다(observed). 아래는 그날의 값이다. -**주입 1 절(파일 넷 편집)과 2 절의 빌드까지 밟았다**(observed). 두 노드에 이미지를 밀어 넣는 데부터는 안 밟았다. - -**★ ⑤ 의 `git diff --stat` 점검은 작업 트리가 깨끗할 때만 성립한다**(2026-09-17, observed). 그 줄은 「네 파일만 나오는가」로 덜 지운 것을 잡으려는 것인데, `keycloak-pattern` 에 이 실험과 무관한 변경이 39개 있어(미추적 3개 더) 그 점검이 무의미해졌다. 따로 떼어낸 작업 트리에서 고치면 그 점검이 다시 살아난다. +**★ ⑤ 의 `git diff --stat` 점검은 작업 트리가 깨끗할 때만 성립한다**(2026-09-17, observed). 그 줄은 「네 파일만 나오는가」로 덜 지운 것을 잡으려는 것인데, `keycloak-pattern` 에 이 실험과 무관한 변경이 39개 있어(미추적 3개 더) 그 점검이 무의미해졌다. 따로 떼어낸 작업 트리에서 고치면 점검이 다시 살아난다. ```bash label="[워크스테이션] 다른 작업이 섞여 있을 때" git worktree add --detach /tmp/kp-b0 HEAD @@ -841,7 +839,7 @@ git worktree add --detach /tmp/kp-b0 HEAD **넷을 다 뺀 뒤에 무엇이 남는지를 숫자로 확인했다**(observed). `pom.xml` 은 XML 로 유효하고 starter 셋(actuator · oauth2-client · web)과 시험 의존 둘만 남는다. `SecurityConfig.java` 에는 `bffSecurity` 하나만 남고 import 일곱이 같이 빠진다. `application.yml` 의 `spring` 아래는 `application` 과 `security` 둘뿐이고, `server.servlet.session` 은 손대지 않는다 — 그것은 쿠키 이름 설정이라 저장소 선택과 무관하다. 매니페스트에서는 env 여섯만 빠지고 Redis Deployment · Service · PVC 는 남는다. -**빌드는 한 번에 통과했다**(observed). 이 편이 앞에 적어 둔 `processDuplicateKeys` 빌드 실패는 다시 나오지 않았다. +**빌드는 한 번에 통과했다**(observed). 이 편이 앞에 적은 `processDuplicateKeys` 빌드 실패는 다시 나오지 않았다. ```text label="docker build 의 끝" [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 @@ -849,13 +847,65 @@ naming to docker.io/library/keycloak-pattern-bff:lab done exit=0 ``` -**안 밟은 것** — `k3s ctr images import` 로 두 노드에 넣는 단계부터다(unknown). 따라서 3 절 배포, 주입 검증, 관찰 3·4 절, 복구 절은 이번에도 원래 실행의 값이다. +### 자동구성이 고른 것 -**그래서 지금 실험대의 BFF 는 여전히 B-1·B-2 상태다**(observed). 빈 목록에 `RedisSessionRepository` · `RedisHttpSessionConfiguration` · `JdbcOAuth2AuthorizedClientService` 가 다 있다. 이 상태에서 replica 2 로 로그인하면 **성공한다** — B-0 은 같은 조건에서 실패한다고 적는다. 두 편을 가르는 것이 세션 저장소 하나임을 반대편에서 확인한 셈이다. +**빈이 321개다**(observed) — 이 편이 예측한 숫자와 같다. 그리고 **`SessionRepository` 빈이 하나도 없다.** -**밟은 관찰** — 관찰 1·2(파드 안에서 빈 목록 받기, 저장소 계열 빈 뽑기)와 5 절의 토큰 경계. `bff/token-boundary` 가 `accessTokenStoredOnServer=true` · `refreshTokenStoredOnServer=true` · `browserTokenCount=0` 을 내는 것은 이 상태에서도 같다(observed). +```text label="[lab host] 빈 목록에서 뽑은 것" +빈 개수: 321 +authorizedClientService -> org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService +OAuth2AuthorizedClientManager -> org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager +authorizedClientRepository -> org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository +SessionRepository 계열 -> 없다 +타입에 Redis 나 Jdbc 가 든 빈 -> 없다 +``` + +`SessionRepository` 가 없다는 것은 Spring Session 이 아예 안 걸렸다는 뜻이고, 그러면 세션을 톰캣이 자기 메모리의 `HttpSession` 에 직접 들고 있다. 토큰 쪽도 `InMemory…` 다. **둘 다 프로세스 메모리에 있고, 아무것도 설정하지 않으면 이렇게 된다.** + +### 로그인은 replica 개수에서 갈린다 + +원 가이드는 브라우저로 한 번 로그인해 보라고 적는데, 그러면 한 번의 결과만 남는다. 쿠키 병을 쓰는 `curl` 로 같은 흐름을 여섯 번씩 돌려 양쪽을 같은 수로 견켜 봤다(observed). + +```text label="replica 2 — 여섯 번 다 실패" +1 https://app1.hyeonworks.com/login?error 200 +2 https://app1.hyeonworks.com/login?error 200 +3 https://app1.hyeonworks.com/login?error 200 +4 https://app1.hyeonworks.com/login?error 200 +5 https://app1.hyeonworks.com/login?error 200 +6 https://app1.hyeonworks.com/login?error 200 +``` + +```text label="replica 1 — 여섯 번 다 성공" +1 https://app1.hyeonworks.com/ 200 +2 https://app1.hyeonworks.com/ 200 +3 https://app1.hyeonworks.com/ 200 +4 https://app1.hyeonworks.com/ 200 +5 https://app1.hyeonworks.com/ 200 +6 https://app1.hyeonworks.com/ 200 +``` + +**여섯 대 여섯으로 갈리니 가설이 확정된다.** 한 번만 치면 replica 2 에서 우연히 같은 파드로 가 성공할 수도 있고, 그러면 반대로 읽힌다. 그리고 replica 1 쪽이 대조군이다 — 거기서도 실패했으면 가설이 아니라 시험 방법이 틀린 것이다. + +토큰 경계는 원래 실행과 한 글자도 다르지 않았다(observed). + +```text label="replica 1 에서 로그인한 뒤 /bff/token-boundary" +{"pattern":"AP3-backend-for-frontend","principal":"labuser","accessTokenStoredOnServer":true,"refreshTokenStoredOnServer":true,"browserTokenCount":0,"csrfProtectionEnabled":true} +``` + +### 원상복구를 같은 명령으로 확인했다 + +replica 를 2 로 되돌리고 예전 이미지를 두 노드에 다시 넣고 원래 매니페스트를 다시 올렸다. 돌아왔는지는 같은 빈 목록과 같은 로그인 시험으로 봤다. + +| 재본 값 | B-0 | 복구 뒤 | +|---|---|---| +| 빈 개수 | `321` | `437` | +| `authorizedClientService` | `InMemoryOAuth2AuthorizedClientService` | `JdbcOAuth2AuthorizedClientService` | +| 타입에 Redis 나 Jdbc 가 든 빈 | `0` | `58` | +| replica 2 로그인 6회 | 6번 다 `/login?error` | 6번 다 `200` | +| Redis 세션 키 | — | `6` | + +**replica 2 에서 다시 로그인이 되는 것이 복구 판정이다.** 그것이 되면 세션이 다시 Redis 를 타고 있다는 뜻이다. -**앞서 적어 둔 「남은 것은 인증서 하나」는 해결됐다**(2026-09-17, observed). 호스트에 있던 인증서를 엣지 게스트로 옮기고 강제 갱신까지 치면서 `https://auth.hyeonworks.com` 은 선다. 지금 남은 걸림돌은 소스 쪽 하나다. ## 무엇이 관측이고 무엇이 아닌가