Stopping Redis returns HTTP 000 rather than an error because the client waits on reconnect, and the pod keeps serving traffic because the redis health indicator is not in the readiness group even though /actuator/health returns 503. That is the mirror image of A-2, where Keycloak put its database check in readiness and the pods left the Service. Turning on AOF with config set created the appendonlydir and still lost everything on pod deletion, because /data was the container filesystem; adding a PVC makes the same setting work. Volume first, persistence setting second. 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
|