Moving the authorized client to JdbcOAuth2AuthorizedClientService makes tokens work across replicas, so the session-in-Redis plus tokens-in-PostgreSQL split holds. The table then shows what sharing cannot fix: the primary key is (client_registration_id, principal_name) with no session in it, so a second login for the same user updates the same row rather than adding one. The refresh token sits in bytea as the raw JWT, readable with convert_from, and logout clears only the Redis session while the plaintext token row and the Keycloak SSO session both survive. The schema itself failed silently first because the default DDL uses blob, which PostgreSQL does not have, and continue-on-error swallowed it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
191 lines
6.8 KiB
YAML
191 lines
6.8 KiB
YAML
# BFF (2 replicas) + Redis, for the B-layer experiments.
|
|
#
|
|
# The BFF is deployed FIRST WITHOUT any session store wiring. That is deliberate:
|
|
# B-0 asks what Spring Boot's autoconfiguration actually picks when nothing is
|
|
# configured, and the only honest way to answer is to look at a running instance
|
|
# that has been given nothing. Redis is deployed alongside but left unused until
|
|
# B-1 turns it on.
|
|
#
|
|
# kubectl apply -f deploy/lab/k8s/bff-redis.yaml
|
|
#
|
|
# Image comes from the workstation, not a registry:
|
|
# docker build -t keycloak-pattern-bff:lab bff/
|
|
# docker save keycloak-pattern-bff:lab | ssh test-server "ssh kc-lab-1 'sudo k3s ctr images import -'"
|
|
# (repeat for kc-lab-2)
|
|
# so imagePullPolicy must stay Never on both replicas.
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: bff-secrets
|
|
namespace: keycloak-lab
|
|
type: Opaque
|
|
stringData:
|
|
# Matches the client created with kcadm in the keycloak-patterns realm.
|
|
# Base64 in etcd is not encryption — see D-3.
|
|
KEYCLOAK_CLIENT_SECRET: bff-lab-secret
|
|
---
|
|
# Redis. No persistence yet: `--save ""` and no appendonly, so a restart loses
|
|
# everything. B-5 and B-6 compare that against RDB and AOF, which is easier to
|
|
# reason about when the starting point is "nothing survives".
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: redis
|
|
namespace: keycloak-lab
|
|
spec:
|
|
replicas: 1
|
|
selector:
|
|
matchLabels: { app: redis }
|
|
template:
|
|
metadata:
|
|
labels: { app: redis }
|
|
spec:
|
|
# Same node as postgres so a node-loss experiment takes both stores at
|
|
# once, matching how A-4 was set up.
|
|
nodeSelector:
|
|
kubernetes.io/hostname: kc-lab-2
|
|
containers:
|
|
- name: redis
|
|
image: redis:7.4-alpine
|
|
args: ["redis-server", "--save", "", "--appendonly", "no"]
|
|
ports:
|
|
- containerPort: 6379
|
|
name: redis
|
|
readinessProbe:
|
|
exec: { command: ["redis-cli", "ping"] }
|
|
initialDelaySeconds: 3
|
|
resources:
|
|
requests: { memory: 32Mi, cpu: 20m }
|
|
limits: { memory: 128Mi }
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: redis
|
|
namespace: keycloak-lab
|
|
spec:
|
|
selector: { app: redis }
|
|
ports:
|
|
- port: 6379
|
|
targetPort: redis
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: bff
|
|
namespace: keycloak-lab
|
|
spec:
|
|
# Two replicas is the whole point: Q1 and Q2 only exist because a request can
|
|
# land on an instance that did not handle the login.
|
|
replicas: 2
|
|
selector:
|
|
matchLabels: { app: bff }
|
|
template:
|
|
metadata:
|
|
labels: { app: bff }
|
|
spec:
|
|
# Spread across both nodes so "the other instance" is genuinely another
|
|
# machine, not another process on the same kernel.
|
|
topologySpreadConstraints:
|
|
- maxSkew: 1
|
|
topologyKey: kubernetes.io/hostname
|
|
whenUnsatisfiable: ScheduleAnyway
|
|
labelSelector:
|
|
matchLabels: { app: bff }
|
|
# 쿠버네티스는 같은 네임스페이스의 Service 마다 Docker link 시절의
|
|
# 환경변수를 자동 주입한다: REDIS_PORT=tcp://10.43.57.116:6379.
|
|
# 그것이 application.yml 의 ${REDIS_PORT:6379} 를 덮어써서 기동이 실패했다.
|
|
# Failed to bind properties under 'spring.data.redis.port' to int:
|
|
# Value: "tcp://10.43.57.116:6379"
|
|
# 이 주입 자체를 끄는 것이 근본 처방이다. 이름을 바꿔 피하면 다음 사람이
|
|
# 같은 함정에 다시 빠진다.
|
|
enableServiceLinks: false
|
|
containers:
|
|
- name: bff
|
|
image: keycloak-pattern-bff:lab
|
|
imagePullPolicy: Never
|
|
ports:
|
|
- containerPort: 8083
|
|
name: http
|
|
env:
|
|
# The browser is redirected to the public name; the BFF calls the
|
|
# token endpoint over the cluster network. Getting these two the same
|
|
# way round is what the 2-hop header experiment was about.
|
|
- 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
|
|
# echo 는 header-lab 네임스페이스의 8081 이다. 다른 네임스페이스의
|
|
# 서비스는 <svc>.<ns>.svc 로 부른다. 이름을 틀리면 500 이 나는데
|
|
# 원인은 UnresolvedAddressException 이지 토큰 문제가 아니다.
|
|
- name: RESOURCE_API_BASE_URL
|
|
value: http://echo.header-lab.svc:8081
|
|
- name: KEYCLOAK_CLIENT_SECRET
|
|
valueFrom:
|
|
secretKeyRef: { name: bff-secrets, key: KEYCLOAK_CLIENT_SECRET }
|
|
# Spring needs to know it is behind TLS termination, for the same
|
|
# reason Keycloak needs KC_PROXY_HEADERS. Without it the redirect_uri
|
|
# it builds comes back as http:// and Keycloak rejects it.
|
|
- name: SERVER_FORWARD_HEADERS_STRATEGY
|
|
value: native
|
|
# B-1: Application Session 을 Redis 로 옮긴다.
|
|
# OAuth2AuthorizedClient 는 이것으로 옮겨지지 않는다 — 조회 키가
|
|
# 다르기 때문이며, B-0 에서 확인한 사실이다.
|
|
- name: SPRING_SESSION_STORE_TYPE
|
|
value: redis
|
|
- name: REDIS_HOST
|
|
value: redis.keycloak-lab.svc
|
|
- name: REDIS_PORT
|
|
value: "6379"
|
|
# B-2: authorized client 는 PostgreSQL 로. 세션(Redis)과 다른
|
|
# 저장소를 쓰는 것이 Q3 가 말한 "각각 설계한다"의 실물이다.
|
|
- name: BFF_DB_URL
|
|
value: jdbc:postgresql://postgres.keycloak-lab.svc:5432/keycloak
|
|
- name: BFF_DB_USER
|
|
value: keycloak
|
|
- name: BFF_DB_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef: { name: keycloak-lab-secrets, key: POSTGRES_PASSWORD }
|
|
- name: JAVA_TOOL_OPTIONS
|
|
value: "-Xms128m -Xmx320m"
|
|
readinessProbe:
|
|
httpGet: { path: /actuator/health/readiness, port: http }
|
|
initialDelaySeconds: 20
|
|
failureThreshold: 30
|
|
livenessProbe:
|
|
httpGet: { path: /actuator/health/liveness, port: http }
|
|
initialDelaySeconds: 60
|
|
resources:
|
|
requests: { memory: 320Mi, cpu: 100m }
|
|
limits: { memory: 512Mi }
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: bff
|
|
namespace: keycloak-lab
|
|
spec:
|
|
selector: { app: bff }
|
|
ports:
|
|
- port: 8083
|
|
targetPort: http
|
|
---
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: bff
|
|
namespace: keycloak-lab
|
|
spec:
|
|
ingressClassName: traefik
|
|
rules:
|
|
- host: app1.hyeonworks.com
|
|
http:
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: bff
|
|
port:
|
|
number: 8083
|