278 lines
8.2 KiB
YAML
278 lines
8.2 KiB
YAML
# Keycloak multi-node cluster with PostgreSQL.
|
|
#
|
|
# Goal of this manifest: two Keycloak pods on two different nodes must discover
|
|
# each other and form one Infinispan cluster. Keycloak 26 discovers peers through
|
|
# the database (jdbc-ping) rather than multicast, writing to a JGROUPS_PING table,
|
|
# but the cluster traffic itself runs over TCP 7800 between the pods. Those are
|
|
# two separate mechanisms, which is why "registered in the DB but not clustered"
|
|
# is a real failure mode — and one that a single node cannot reproduce.
|
|
#
|
|
# kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
|
|
# kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=600s
|
|
#
|
|
# Secrets are plain here. Proper secret handling is roadmap item 11; keeping it
|
|
# visible for now is deliberate so the gap is obvious rather than forgotten.
|
|
apiVersion: v1
|
|
kind: Namespace
|
|
metadata:
|
|
name: keycloak-lab
|
|
---
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: keycloak-lab-secrets
|
|
namespace: keycloak-lab
|
|
type: Opaque
|
|
stringData:
|
|
POSTGRES_PASSWORD: lab-postgres-change-me
|
|
KC_BOOTSTRAP_ADMIN_PASSWORD: lab-admin-change-me
|
|
---
|
|
# PostgreSQL. local-path binds the volume to whichever node the pod lands on, so
|
|
# the database is effectively pinned to one node. That is not a flaw here: it is
|
|
# what makes "the database node dies" a meaningful experiment later.
|
|
apiVersion: v1
|
|
kind: PersistentVolumeClaim
|
|
metadata:
|
|
name: postgres-data
|
|
namespace: keycloak-lab
|
|
spec:
|
|
accessModes: [ReadWriteOnce]
|
|
storageClassName: local-path
|
|
resources:
|
|
requests:
|
|
storage: 5Gi
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: postgres
|
|
namespace: keycloak-lab
|
|
spec:
|
|
replicas: 1
|
|
strategy:
|
|
type: Recreate # RWO volume cannot be mounted by two pods at once
|
|
selector:
|
|
matchLabels:
|
|
app: postgres
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: postgres
|
|
spec:
|
|
containers:
|
|
- name: postgres
|
|
image: postgres:16-alpine
|
|
ports:
|
|
- containerPort: 5432
|
|
name: postgres
|
|
env:
|
|
- name: POSTGRES_DB
|
|
value: keycloak
|
|
- name: POSTGRES_USER
|
|
value: keycloak
|
|
- name: POSTGRES_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-lab-secrets
|
|
key: POSTGRES_PASSWORD
|
|
# The image refuses to initialise into a non-empty mount, and
|
|
# local-path volumes are clean, but this keeps the data one level
|
|
# down so a lost+found or similar never blocks initdb.
|
|
- name: PGDATA
|
|
value: /var/lib/postgresql/data/pgdata
|
|
volumeMounts:
|
|
- name: data
|
|
mountPath: /var/lib/postgresql/data
|
|
readinessProbe:
|
|
exec:
|
|
command: ["sh", "-c", "pg_isready -U keycloak -d keycloak"]
|
|
initialDelaySeconds: 10
|
|
periodSeconds: 5
|
|
resources:
|
|
requests:
|
|
memory: 192Mi
|
|
cpu: 50m
|
|
limits:
|
|
memory: 512Mi
|
|
volumes:
|
|
- name: data
|
|
persistentVolumeClaim:
|
|
claimName: postgres-data
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: postgres
|
|
namespace: keycloak-lab
|
|
spec:
|
|
selector:
|
|
app: postgres
|
|
ports:
|
|
- port: 5432
|
|
targetPort: postgres
|
|
---
|
|
# Keycloak. A StatefulSet rather than a Deployment so each pod keeps a stable
|
|
# name (keycloak-0, keycloak-1); cluster membership is far easier to read in
|
|
# logs and in the JGROUPS_PING table when the identities do not churn.
|
|
apiVersion: apps/v1
|
|
kind: StatefulSet
|
|
metadata:
|
|
name: keycloak
|
|
namespace: keycloak-lab
|
|
spec:
|
|
serviceName: keycloak-headless
|
|
replicas: 2
|
|
podManagementPolicy: Parallel # both pods start together, so they race to
|
|
# register — which is the interesting case
|
|
selector:
|
|
matchLabels:
|
|
app: keycloak
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: keycloak
|
|
spec:
|
|
# One pod per node. Two pods on one node would share a kernel and make the
|
|
# 7800 blocking experiment meaningless.
|
|
topologySpreadConstraints:
|
|
- maxSkew: 1
|
|
topologyKey: kubernetes.io/hostname
|
|
whenUnsatisfiable: ScheduleAnyway
|
|
labelSelector:
|
|
matchLabels:
|
|
app: keycloak
|
|
containers:
|
|
- name: keycloak
|
|
image: quay.io/keycloak/keycloak:26.7.0
|
|
# "start", not "start-dev". Dev mode forces cache=local and there is
|
|
# no cluster to form at all.
|
|
args: ["start"]
|
|
ports:
|
|
- containerPort: 8080
|
|
name: http
|
|
- containerPort: 9000
|
|
name: management
|
|
- containerPort: 7800
|
|
name: jgroups
|
|
env:
|
|
- name: KC_DB
|
|
value: postgres
|
|
- name: KC_DB_URL
|
|
value: jdbc:postgresql://postgres:5432/keycloak
|
|
- name: KC_DB_USERNAME
|
|
value: keycloak
|
|
- name: KC_DB_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-lab-secrets
|
|
key: POSTGRES_PASSWORD
|
|
|
|
# Settings confirmed by the two-hop header measurement.
|
|
# KC_HOSTNAME carries the full external URL, which pins scheme and
|
|
# host for issuer and redirect URLs regardless of headers.
|
|
# KC_PROXY_HEADERS is the separate opt-in that lets the forwarded
|
|
# client address through — the same kind of switch as Spring's
|
|
# forward-headers-strategy. See docs/two-hop-proxy-header-contract.md.
|
|
- name: KC_HOSTNAME
|
|
value: https://auth.hyeonworks.com
|
|
- name: KC_HOSTNAME_STRICT
|
|
value: "true"
|
|
- name: KC_PROXY_HEADERS
|
|
value: xforwarded
|
|
- name: KC_HTTP_ENABLED
|
|
value: "true"
|
|
|
|
- name: KC_HEALTH_ENABLED
|
|
value: "true"
|
|
- name: KC_METRICS_ENABLED
|
|
value: "true"
|
|
|
|
# Without an explicit cap the JVM sizes its heap from the container
|
|
# limit and this lab has roughly 3.8GB of guest headroom in total.
|
|
- name: JAVA_OPTS_KC_HEAP
|
|
value: "-Xms256m -Xmx512m"
|
|
|
|
- name: KC_BOOTSTRAP_ADMIN_USERNAME
|
|
value: admin
|
|
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: keycloak-lab-secrets
|
|
key: KC_BOOTSTRAP_ADMIN_PASSWORD
|
|
|
|
# Keycloak serves health and metrics on the management port (9000),
|
|
# not on 8080, since version 25.
|
|
startupProbe:
|
|
httpGet:
|
|
path: /health/started
|
|
port: management
|
|
periodSeconds: 10
|
|
failureThreshold: 60 # first boot runs an implicit build
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /health/ready
|
|
port: management
|
|
periodSeconds: 10
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /health/live
|
|
port: management
|
|
periodSeconds: 30
|
|
resources:
|
|
requests:
|
|
memory: 640Mi
|
|
cpu: 100m
|
|
limits:
|
|
memory: 900Mi
|
|
---
|
|
# Headless service. Not required for jdbc-ping discovery, which goes through the
|
|
# database, but it gives each pod a stable DNS name for direct inspection.
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: keycloak-headless
|
|
namespace: keycloak-lab
|
|
spec:
|
|
clusterIP: None
|
|
selector:
|
|
app: keycloak
|
|
ports:
|
|
- port: 8080
|
|
targetPort: http
|
|
name: http
|
|
- port: 9000
|
|
targetPort: management
|
|
name: management
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: keycloak
|
|
namespace: keycloak-lab
|
|
spec:
|
|
selector:
|
|
app: keycloak
|
|
ports:
|
|
- port: 8080
|
|
targetPort: http
|
|
name: http
|
|
---
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: keycloak
|
|
namespace: keycloak-lab
|
|
spec:
|
|
ingressClassName: traefik
|
|
rules:
|
|
- host: auth.hyeonworks.com
|
|
http:
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: keycloak
|
|
port:
|
|
number: 8080
|