The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
215 lines
7.5 KiB
YAML
215 lines
7.5 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. B-5 measured that turning on AOF with `redis-cli config set` changes
|
|
# nothing here, because /data is the container filesystem and dies with the
|
|
# container — the appendonlydir was created and then thrown away. Persistence
|
|
# configuration without a volume is decoration.
|
|
#
|
|
# So the volume comes first, and only then does `--appendonly yes` mean anything.
|
|
apiVersion: v1
|
|
kind: PersistentVolumeClaim
|
|
metadata:
|
|
name: redis-data
|
|
namespace: keycloak-lab
|
|
spec:
|
|
accessModes: [ReadWriteOnce]
|
|
storageClassName: local-path
|
|
resources:
|
|
requests:
|
|
storage: 1Gi
|
|
---
|
|
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
|
|
# appendfsync everysec 이 기본값이다 — 1초 분량을 잃을 수 있다.
|
|
# Keycloak 의 synchronous_commit OFF(A-3)와 같은 모양의 트레이드오프다.
|
|
args: ["redis-server", "--appendonly", "yes", "--dir", "/data"]
|
|
ports:
|
|
- containerPort: 6379
|
|
name: redis
|
|
readinessProbe:
|
|
exec: { command: ["redis-cli", "ping"] }
|
|
initialDelaySeconds: 3
|
|
volumeMounts:
|
|
- name: data
|
|
mountPath: /data
|
|
resources:
|
|
requests: { memory: 32Mi, cpu: 20m }
|
|
limits: { memory: 128Mi }
|
|
volumes:
|
|
- name: data
|
|
persistentVolumeClaim:
|
|
claimName: redis-data
|
|
---
|
|
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
|