1. HA Architecture Overview
Keycloak High Availability đảm bảo hệ thống authentication/authorization hoạt động liên tục, ngay cả khi một hoặc nhiều nodes gặp sự cố. HA dựa trên ba trụ cột: clustering (Infinispan), database replication, và load balancing.
┌──────────────────────────────────────────────────────────────────┐
│ High Availability Architecture │
│ │
│ ┌──────────────┐ │
│ │ Load Balancer │ ←── Sticky Sessions (KEYCLOAK_SESSION) │
│ │ (HAProxy/ │ │
│ │ Nginx/ALB) │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────┴─────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Keycloak 1│ │Keycloak 2│ │Keycloak 3│ ←── Embedded Infinispan │
│ │ │◄─► │◄─► │ (cache replication) │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ PostgreSQL │ ←── Primary-Replica Replication │
│ │ (Primary) │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌────────▼─────────┐ │
│ │ PostgreSQL │ │
│ │ (Replica) │ │
│ └──────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
2. Infinispan Cache Types
2.1 Local vs Distributed/Replicated Caches
Keycloak sử dụng hai loại cache Infinispan với mục đích khác nhau:
| Cache Type | Mô tả | Keycloak Caches | Cluster Mode |
|---|---|---|---|
| Local Cache | Lưu trữ data trên node hiện tại, dùng cho metadata có thể đọc lại từ DB | realms, users, authorization, keys | Local + invalidation messages |
| Distributed Cache | Data phân tán trên N owners trong cluster, dùng cho session data | sessions, authenticationSessions, offlineSessions, clientSessions, offlineClientSessions | Distributed (2 owners default) |
| Replicated Cache | Data replicated trên tất cả nodes | work (cluster communication) | Replicated |
2.2 Keycloak Caching Architecture Chi tiết
┌─────────────────────────────────────────────────────────────────────┐
│ Keycloak Cache Architecture │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ LOCAL CACHES (mỗi node giữ copy riêng, invalidation khi thay đổi)│
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ realms │ │ users │ │authorization │ │ keys │ │
│ │(realm cfg│ │(user data│ │(permissions) │ │(crypto │ │
│ │ metadata)│ │ profile) │ │ │ │ keys) │ │
│ └──────────┘ └──────────┘ └──────────────┘ └──────────┘ │
│ │
│ DISTRIBUTED CACHES (session data phân tán trong cluster) │
│ ┌───────────────────┐ ┌──────────────────────────┐ │
│ │ sessions │ │ authenticationSessions │ │
│ │ (user SSO sessions)│ │ (login flow state) │ │
│ └───────────────────┘ └──────────────────────────┘ │
│ ┌───────────────────┐ ┌──────────────────────────┐ │
│ │ offlineSessions │ │ loginFailures │ │
│ │ (offline tokens) │ │ (brute force tracking) │ │
│ └───────────────────┘ └──────────────────────────┘ │
│ ┌───────────────────┐ │
│ │ actionTokens │ │
│ │ (email verify, │ │
│ │ reset password) │ │
│ └───────────────────┘ │
│ │
│ REPLICATED CACHES │
│ ┌───────────────────┐ │
│ │ work │ (cluster-wide notifications) │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
3. Embedded vs External Infinispan
3.1 Embedded Infinispan (Default)
Mặc định, Keycloak nhúng Infinispan trong JVM process. Các node phát hiện nhau qua discovery protocol và tự động form cluster:
# Embedded Infinispan (default) - không cần cấu hình thêm
bin/kc.sh start --optimized \
--cache=ispn
3.2 External Infinispan
Cho multi-site deployment hoặc khi cần quản lý cache layer riêng biệt, sử dụng External Infinispan server:
# Keycloak kết nối External Infinispan
bin/kc.sh start --optimized \
--cache=ispn \
--cache-config-file=cache-ispn-remote.xml \
--spi-connections-infinispan-quarkus-site-name=site1
| Feature | Embedded Infinispan | External Infinispan |
|---|---|---|
| Setup complexity | Đơn giản, zero config | Cần deploy Infinispan cluster riêng |
| Multi-site | ❌ Không hỗ trợ | ✅ Cross-datacenter replication |
| Scalability | Tốt cho ≤ 10 nodes | Tốt cho large-scale deployments |
| Management | Tự động | Cần monitor/manage Infinispan riêng |
| Use case | Single-site, single cluster | Multi-site, DR, large deployments |
4. Cache Stack Configuration
4.1 Kubernetes (KUBE_PING)
Trên Kubernetes, sử dụng KUBE_PING (dns.DNS_PING) để các Keycloak pods tự phát hiện nhau qua Kubernetes API hoặc DNS:
# Keycloak trên Kubernetes - dùng dns cache stack
bin/kc.sh start --optimized \
--cache=ispn \
--cache-stack=kubernetes
Keycloak Kubernetes cần một headless Service để DNS discovery hoạt động:
# headless-service.yaml - cho Infinispan cluster discovery
apiVersion: v1
kind: Service
metadata:
name: keycloak-headless
namespace: keycloak
spec:
type: ClusterIP
clusterIP: None # Headless service
publishNotReadyAddresses: true
selector:
app: keycloak
ports:
- name: jgroups
port: 7800
targetPort: 7800
protocol: TCP
# Environment variable cho DNS_PING
export KC_CACHE_STACK=kubernetes
export JAVA_OPTS_APPEND="-Djgroups.dns.query=keycloak-headless.keycloak.svc.cluster.local"
4.2 JDBC-PING (cho VM Deployments)
Khi deploy trên VMs (không có Kubernetes), sử dụng JDBC_PING để các node phát hiện nhau qua shared database:
<?xml version="1.0" encoding="UTF-8"?>
<!-- cache-ispn-jdbc-ping.xml -->
<infinispan
xmlns="urn:infinispan:config:15.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:infinispan:config:15.0
https://infinispan.org/schemas/infinispan-config-15.0.xsd">
<jgroups>
<stack name="jdbc-ping-stack">
<TCP bind_addr="match-interface:eth0"
bind_port="7800"
recv_buf_size="20M"
send_buf_size="640K"/>
<JDBC_PING connection_url="jdbc:postgresql://db-host:5432/keycloak"
connection_username="keycloak"
connection_password="secure_password"
connection_driver="org.postgresql.Driver"
initialize_sql="CREATE TABLE IF NOT EXISTS JGROUPSPING (
own_addr varchar(200) NOT NULL,
cluster_name varchar(200) NOT NULL,
ping_data BYTEA,
constraint PK_JGROUPSPING PRIMARY KEY (own_addr, cluster_name)
)"
insert_single_sql="INSERT INTO JGROUPSPING (own_addr, cluster_name, ping_data)
VALUES (?, ?, ?)"
delete_single_sql="DELETE FROM JGROUPSPING
WHERE own_addr=? AND cluster_name=?"
select_all_pingdata_sql="SELECT ping_data
FROM JGROUPSPING WHERE cluster_name=?"/>
<MERGE3 min_interval="10000" max_interval="30000"/>
<FD_SOCK/>
<FD_ALL timeout="60000" interval="15000"/>
<VERIFY_SUSPECT timeout="5000"/>
<pbcast.NAKACK2 use_mcast_xmit="false"/>
<UNICAST3/>
<pbcast.STABLE/>
<pbcast.GMS join_timeout="5000"/>
<MFC max_credits="2M" min_threshold="0.4"/>
<FRAG3 frag_size="60K"/>
</stack>
</jgroups>
<cache-container name="keycloak">
<transport lock-timeout="60000" stack="jdbc-ping-stack"/>
<!-- Cache definitions... -->
</cache-container>
</infinispan>
4.3 DNS-PING
# DNS-PING cho Docker Swarm hoặc Consul DNS
export JAVA_OPTS_APPEND="-Djgroups.dns.query=keycloak-tasks.keycloak"
bin/kc.sh start --optimized \
--cache=ispn \
--cache-stack=kubernetes # DNS_PING nằm trong kubernetes stack
5. External Infinispan Server Setup
5.1 Infinispan Server Configuration
<!-- infinispan.xml cho External Infinispan Server -->
<infinispan
xmlns="urn:infinispan:config:15.0"
xmlns:server="urn:infinispan:server:15.0">
<cache-container name="keycloak" statistics="true">
<transport cluster="keycloak-cluster"
stack="tcp"
node-name="${infinispan.node.name:node1}"/>
<!-- Distributed caches cho Keycloak sessions -->
<distributed-cache name="sessions" owners="2" mode="SYNC">
<encoding>
<key media-type="application/x-protostream"/>
<value media-type="application/x-protostream"/>
</encoding>
<memory max-count="-1"/>
<persistence passivation="false">
<!-- Optional: persist to disk -->
</persistence>
</distributed-cache>
<distributed-cache name="authenticationSessions" owners="2" mode="SYNC">
<encoding>
<key media-type="application/x-protostream"/>
<value media-type="application/x-protostream"/>
</encoding>
</distributed-cache>
<distributed-cache name="offlineSessions" owners="2" mode="SYNC">
<encoding>
<key media-type="application/x-protostream"/>
<value media-type="application/x-protostream"/>
</encoding>
</distributed-cache>
<distributed-cache name="clientSessions" owners="2" mode="SYNC">
<encoding>
<key media-type="application/x-protostream"/>
<value media-type="application/x-protostream"/>
</encoding>
</distributed-cache>
<distributed-cache name="offlineClientSessions" owners="2" mode="SYNC">
<encoding>
<key media-type="application/x-protostream"/>
<value media-type="application/x-protostream"/>
</encoding>
</distributed-cache>
<distributed-cache name="loginFailures" owners="2" mode="SYNC">
<encoding>
<key media-type="application/x-protostream"/>
<value media-type="application/x-protostream"/>
</encoding>
</distributed-cache>
<distributed-cache name="actionTokens" owners="2" mode="SYNC">
<encoding>
<key media-type="application/x-protostream"/>
<value media-type="application/x-protostream"/>
</encoding>
<expiration max-idle="-1" lifespan="-1" interval="300000"/>
</distributed-cache>
<replicated-cache name="work" mode="SYNC">
<encoding>
<key media-type="application/x-protostream"/>
<value media-type="application/x-protostream"/>
</encoding>
</replicated-cache>
</cache-container>
<server xmlns="urn:infinispan:server:15.0">
<interfaces>
<interface name="public">
<inet-address value="${infinispan.bind.address:0.0.0.0}"/>
</interface>
</interfaces>
<socket-bindings default-interface="public" port-offset="0">
<socket-binding name="default" port="11222"/>
</socket-bindings>
<security>
<security-realms>
<security-realm name="default">
<properties-realm groups-attribute="Roles">
<user-properties path="users.properties"/>
<group-properties path="groups.properties"/>
</properties-realm>
</security-realm>
</security-realms>
</security>
<endpoints>
<endpoint socket-binding="default" security-realm="default">
<hotrod-connector name="hotrod"/>
<rest-connector name="rest"/>
</endpoint>
</endpoints>
</server>
</infinispan>
5.2 Keycloak Remote Cache Config
Cấu hình Keycloak kết nối đến External Infinispan:
<?xml version="1.0" encoding="UTF-8"?>
<!-- cache-ispn-remote.xml - Keycloak side -->
<infinispan
xmlns="urn:infinispan:config:15.0">
<cache-container name="keycloak">
<transport lock-timeout="60000"/>
<!-- Local caches (giữ nguyên trên mỗi Keycloak node) -->
<local-cache name="realms">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory max-count="10000"/>
</local-cache>
<local-cache name="users">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory max-count="10000"/>
</local-cache>
<local-cache name="authorization">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory max-count="10000"/>
</local-cache>
<local-cache name="keys">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory max-count="1000"/>
<expiration max-idle="3600000"/>
</local-cache>
<!-- Distributed caches backed by remote Infinispan server -->
<distributed-cache name="sessions" owners="2">
<remote-store xmlns="urn:infinispan:config:store:remote:15.0"
cache="sessions"
purge="false"
preload="false"
shared="true"
segmented="false"
raw-values="true">
<remote-server host="infinispan-server-1" port="11222"/>
<remote-server host="infinispan-server-2" port="11222"/>
<security>
<authentication>
<digest username="keycloak" password="changeme" realm="default"/>
</authentication>
</security>
</remote-store>
</distributed-cache>
<distributed-cache name="authenticationSessions" owners="2">
<remote-store xmlns="urn:infinispan:config:store:remote:15.0"
cache="authenticationSessions"
purge="false" preload="false" shared="true"
segmented="false" raw-values="true">
<remote-server host="infinispan-server-1" port="11222"/>
<remote-server host="infinispan-server-2" port="11222"/>
<security>
<authentication>
<digest username="keycloak" password="changeme" realm="default"/>
</authentication>
</security>
</remote-store>
</distributed-cache>
<distributed-cache name="offlineSessions" owners="2">
<remote-store xmlns="urn:infinispan:config:store:remote:15.0"
cache="offlineSessions"
purge="false" preload="false" shared="true"
segmented="false" raw-values="true">
<remote-server host="infinispan-server-1" port="11222"/>
<remote-server host="infinispan-server-2" port="11222"/>
<security>
<authentication>
<digest username="keycloak" password="changeme" realm="default"/>
</authentication>
</security>
</remote-store>
</distributed-cache>
</cache-container>
</infinispan>
6. Multi-site Deployment
6.1 Active-Passive Pattern
Trong active-passive, chỉ primary site phục vụ traffic. Backup site ở trạng thái standby, sẵn sàng take over khi primary gặp sự cố:
┌──────────────────────────────────────────────────────────────────────┐
│ Active-Passive Multi-site │
│ │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ Site A (Active) │ │ Site B (Passive) │ │
│ │ │ │ │ │
│ │ ┌──────┐ ┌──────┐ │ │ ┌──────┐ ┌──────┐ │ │
│ │ │ KC-1 │ │ KC-2 │ │ │ │ KC-3 │ │ KC-4 │ │ │
│ │ └──┬───┘ └──┬───┘ │ │ └──┬───┘ └──┬───┘ │ │
│ │ │ │ │ │ │ │ │ │
│ │ ┌──▼─────────▼───┐ │ │ ┌──▼─────────▼───┐ │ │
│ │ │ Infinispan │◄─────────►│ Infinispan │ │ │
│ │ │ (External) │ Cross- │ │ (External) │ │ │
│ │ └────────────────┘ Site │ └────────────────┘ │ │
│ │ Repl. │ │ │
│ │ ┌────────────────┐ │ │ ┌────────────────┐ │ │
│ │ │ PostgreSQL │◄─────────►│ PostgreSQL │ │ │
│ │ │ (Primary) │ Repl. │ │ (Standby) │ │ │
│ │ └────────────────┘ │ │ └────────────────┘ │ │
│ └────────────────────────┘ └────────────────────────┘ │
│ │
│ DNS: auth.example.com ──► Site A (failover to Site B) │
└──────────────────────────────────────────────────────────────────────┘
6.2 Active-Active Pattern
Trong active-active, cả hai sites đều phục vụ traffic đồng thời. Phức tạp hơn nhưng tận dụng tối đa resources:
┌──────────────────────────────────────────────────────────────────────┐
│ Active-Active Multi-site │
│ │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ Site A (Active) │ │ Site B (Active) │ │
│ │ │ │ │ │
│ │ ┌──────┐ ┌──────┐ │ │ ┌──────┐ ┌──────┐ │ │
│ │ │ KC-1 │ │ KC-2 │ │ │ │ KC-3 │ │ KC-4 │ │ │
│ │ └──┬───┘ └──┬───┘ │ │ └──┬───┘ └──┬───┘ │ │
│ │ └────┬────┘ │ │ └────┬────┘ │ │
│ │ ┌───────▼────────┐ │ │ ┌───────▼────────┐ │ │
│ │ │ Infinispan │◄═══════════►│ Infinispan │ │ │
│ │ │ (Cross-site) │ RELAY │ │ (Cross-site) │ │ │
│ │ └────────────────┘ │ │ └────────────────┘ │ │
│ │ │ │ │ │
│ │ ┌────────────────┐ │ │ ┌────────────────┐ │ │
│ │ │ PostgreSQL │◄═══════════►│ PostgreSQL │ │ │
│ │ │ (Multi-master) │ Sync │ │ (Multi-master) │ │ │
│ │ └────────────────┘ │ │ └────────────────┘ │ │
│ └────────────────────────┘ └────────────────────────┘ │
│ │
│ GSLB: auth.example.com ──► Site A (50%) + Site B (50%) │
└──────────────────────────────────────────────────────────────────────┘
| Feature | Active-Passive | Active-Active |
|---|---|---|
| Traffic handling | 1 site tại một thời điểm | Cả 2 sites đồng thời |
| Resource utilization | 50% (passive site idle) | 100% |
| Complexity | Thấp hơn | Cao hơn (conflict resolution) |
| Failover time | Cần DNS switch (seconds-minutes) | Tự động (GSLB) |
| Data consistency | Strong (synchronous repl) | Eventual (async cross-site) |
| DB requirement | Primary-Standby | Multi-master hoặc shared DB |
6.3 Cross-Datacenter Infinispan Configuration
<!-- infinispan-xsite.xml - Site A Infinispan Server -->
<infinispan xmlns="urn:infinispan:config:15.0"
xmlns:server="urn:infinispan:server:15.0">
<jgroups>
<!-- Local cluster stack -->
<stack name="tcp" extends="tcp">
<!-- Node discovery within same site -->
</stack>
<!-- Cross-site (relay) stack -->
<stack name="relay">
<TCP bind_addr="match-interface:eth0" bind_port="7900"/>
<TCPPING initial_hosts="infinispan-siteA:7900,infinispan-siteB:7900"
port_range="0"/>
<MERGE3/>
<FD_ALL/>
<VERIFY_SUSPECT/>
<pbcast.NAKACK2/>
<UNICAST3/>
<pbcast.STABLE/>
<pbcast.GMS/>
</stack>
</jgroups>
<cache-container name="keycloak" statistics="true">
<transport cluster="keycloak-cluster"
stack="tcp"
node-name="${infinispan.node.name}"
site="${infinispan.site.name:siteA}">
<!-- Relay configuration for cross-site -->
<relay site="${infinispan.site.name:siteA}">
<remote-site name="siteA" stack="relay"/>
<remote-site name="siteB" stack="relay"/>
</relay>
</transport>
<!-- Sessions cache with cross-site backup -->
<distributed-cache name="sessions" owners="2" mode="SYNC">
<encoding>
<key media-type="application/x-protostream"/>
<value media-type="application/x-protostream"/>
</encoding>
<backups>
<backup site="siteB" strategy="ASYNC"
failure-policy="WARN"
timeout="15000"/>
</backups>
</distributed-cache>
<distributed-cache name="authenticationSessions" owners="2" mode="SYNC">
<encoding>
<key media-type="application/x-protostream"/>
<value media-type="application/x-protostream"/>
</encoding>
<backups>
<backup site="siteB" strategy="ASYNC"
failure-policy="WARN"
timeout="15000"/>
</backups>
</distributed-cache>
<!-- Thêm các distributed caches khác tương tự... -->
</cache-container>
</infinispan>
7. Database Replication
7.1 PostgreSQL Patroni + PgBouncer
Patroni quản lý PostgreSQL HA (automatic failover), PgBouncer cung cấp connection pooling:
┌─────────────────────────────────────────────────────────────┐
│ PostgreSQL HA with Patroni + PgBouncer │
│ │
│ ┌──────────────┐ │
│ │ Keycloak │ │
│ │ Instances │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ PgBouncer │ ←── Connection pooling │
│ │ (VIP/DNS) │ Transaction mode │
│ └──────┬───────┘ │
│ │ │
│ ┌──────┴────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ PG Node1│ │ PG Node2│ │ PG Node3│ │
│ │(Primary)│ │(Replica)│ │(Replica)│ │
│ │ Patroni │ │ Patroni │ │ Patroni │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └────────────┼────────────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ etcd │ ←── Consensus store │
│ │ cluster │ (leader election) │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
7.2 Patroni Configuration
# patroni.yml - Patroni configuration cho Keycloak
scope: keycloak-cluster
name: pg-node1
restapi:
listen: 0.0.0.0:8008
connect_address: pg-node1:8008
etcd3:
hosts:
- etcd1:2379
- etcd2:2379
- etcd3:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
synchronous_mode: true # Synchronous replication
synchronous_mode_strict: false
postgresql:
use_pg_rewind: true
parameters:
max_connections: 400
shared_buffers: 2GB
effective_cache_size: 6GB
work_mem: 16MB
wal_level: replica
max_wal_senders: 5
max_replication_slots: 5
hot_standby: "on"
synchronous_commit: "on" # Synchronous cho data safety
initdb:
- encoding: UTF8
- data-checksums
postgresql:
listen: 0.0.0.0:5432
connect_address: pg-node1:5432
data_dir: /var/lib/postgresql/data
authentication:
superuser:
username: postgres
password: postgres_password
replication:
username: replicator
password: replicator_password
rewind:
username: rewinder
password: rewinder_password
parameters:
unix_socket_directories: "/var/run/postgresql"
7.3 PgBouncer Configuration
# pgbouncer.ini
[databases]
keycloak = host=pg-primary port=5432 dbname=keycloak
auth_user=keycloak
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
# Connection pooling
pool_mode = transaction # Transaction mode cho Keycloak
max_client_conn = 1000
default_pool_size = 100
min_pool_size = 25
reserve_pool_size = 10
reserve_pool_timeout = 3
# Timeouts
server_connect_timeout = 15
server_idle_timeout = 600
server_lifetime = 3600
client_idle_timeout = 0
client_login_timeout = 60
query_timeout = 0
query_wait_timeout = 120
# Logging
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
stats_period = 60
8. Load Balancer Configuration
8.1 Sticky Sessions
Keycloak yêu cầu sticky sessions cho authentication flows (login, registration). Session affinity dựa trên cookie KC_ROUTE (trước đây KEYCLOAK_SESSION):
8.2 HAProxy Configuration
# haproxy.cfg cho Keycloak HA
global
log stdout format raw daemon
maxconn 4096
defaults
mode http
log global
option httplog
option dontlognull
option http-server-close
option forwardfor except 127.0.0.0/8
retries 3
timeout http-request 10s
timeout queue 60s
timeout connect 10s
timeout client 60s
timeout server 60s
timeout http-keep-alive 10s
timeout check 10s
# Frontend HTTPS
frontend keycloak_frontend
bind *:443 ssl crt /etc/haproxy/certs/auth.example.com.pem
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Port 443
# Health check endpoint (không cần sticky)
acl is_health path_beg /health
acl is_metrics path_beg /metrics
use_backend keycloak_health if is_health
use_backend keycloak_health if is_metrics
default_backend keycloak_backend
# Backend với sticky sessions
backend keycloak_backend
balance roundrobin
# Sticky session dựa trên KC_ROUTE cookie
cookie KC_ROUTE insert indirect nocache httponly secure
option httpchk GET /health/ready
http-check expect status 200
# Keycloak servers
server kc1 keycloak-1:8443 ssl verify none check inter 10s \
fall 3 rise 2 cookie kc1
server kc2 keycloak-2:8443 ssl verify none check inter 10s \
fall 3 rise 2 cookie kc2
server kc3 keycloak-3:8443 ssl verify none check inter 10s \
fall 3 rise 2 cookie kc3
# Backend cho health/metrics (không cần sticky)
backend keycloak_health
balance roundrobin
option httpchk GET /health/ready
server kc1 keycloak-1:8443 ssl verify none check
server kc2 keycloak-2:8443 ssl verify none check
server kc3 keycloak-3:8443 ssl verify none check
# Stats dashboard
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 10s
stats auth admin:admin_password
8.3 Nginx Load Balancer
# nginx.conf - Keycloak load balancer
upstream keycloak_cluster {
# Sticky sessions via cookie
sticky cookie KC_ROUTE expires=1h domain=.example.com httponly secure;
server keycloak-1:8443 max_fails=3 fail_timeout=30s;
server keycloak-2:8443 max_fails=3 fail_timeout=30s;
server keycloak-3:8443 max_fails=3 fail_timeout=30s;
}
server {
listen 443 ssl http2;
server_name auth.example.com;
ssl_certificate /etc/nginx/certs/tls.crt;
ssl_certificate_key /etc/nginx/certs/tls.key;
# Buffer sizes cho Keycloak
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
location / {
proxy_pass https://keycloak_cluster;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Health check (no sticky needed)
location /health {
proxy_pass https://keycloak_cluster;
proxy_set_header Host $host;
}
}
9. Split-Brain Handling
9.1 Split-Brain Scenarios
Split-brain xảy ra khi network partition chia cluster thành 2+ partitions, mỗi partition nghĩ phía còn lại đã chết:
┌─────────────────────────────────────────────────────────────┐
│ Split-Brain Scenario │
│ │
│ Partition A ║ Partition B │
│ ┌──────┐ ┌──────┐ ║ ┌──────┐ ┌──────┐ │
│ │ KC-1 │◄─►│ KC-2 │ ║ │ KC-3 │◄─►│ KC-4 │ │
│ └──────┘ └──────┘ ║ └──────┘ └──────┘ │
│ ↕ ║ ↕ │
│ ┌──────────┐ ║ ┌──────────┐ │
│ │ PG Primary│ ║ │ PG Replica│ ← Có thể │
│ └──────────┘ ║ └──────────┘ promote sai! │
│ ║ │
│ Network Partition ═══════╝ │
└─────────────────────────────────────────────────────────────┘
9.2 Merge Policies
<!-- Infinispan merge policy configuration -->
<distributed-cache name="sessions" owners="2" mode="SYNC">
<partition-handling when-split="ALLOW_READ_WRITES"
merge-policy="PREFERRED_NON_NULL"/>
</distributed-cache>
| Merge Policy | Behavior | Use Case |
|---|---|---|
PREFERRED_NON_NULL | Giữ entry non-null khi merge | Sessions - tốt hơn keep session hơn mất |
REMOVE_ALL | Xóa tất cả conflicting entries | Khi data consistency quan trọng nhất |
PREFERRED_ALWAYS | Giữ entry từ partition lớn hơn | General purpose |
10. Disaster Recovery Strategies
10.1 RPO/RTO Targets
| Tier | RPO | RTO | Strategy |
|---|---|---|---|
| Tier 1 (Critical) | 0 (zero data loss) | < 5 minutes | Active-Active + Synchronous DB replication |
| Tier 2 (Important) | < 1 minute | < 15 minutes | Active-Passive + Async replication + automated failover |
| Tier 3 (Standard) | < 1 hour | < 1 hour | Backup/Restore + manual failover |
10.2 Backup & Restore
#!/bin/bash
# backup-keycloak.sh - Automated backup script
BACKUP_DIR="/backups/keycloak/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
# 1. Database backup (PostgreSQL)
echo "=== Backing up database ==="
pg_dump -h db-host -U keycloak -d keycloak \
--format=custom \
--compress=9 \
--file="$BACKUP_DIR/keycloak-db.dump"
# 2. Realm export via Admin API
echo "=== Exporting realms ==="
# Lấy admin token
ADMIN_TOKEN=$(curl -s \
-d "client_id=admin-cli" \
-d "username=admin" \
-d "password=admin_password" \
-d "grant_type=password" \
"https://auth.example.com/realms/master/protocol/openid-connect/token" \
| jq -r '.access_token')
# Export từng realm
for REALM in $(curl -s \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"https://auth.example.com/admin/realms" \
| jq -r '.[].realm'); do
echo "Exporting realm: $REALM"
curl -s \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"https://auth.example.com/admin/realms/$REALM/partial-export?exportClients=true&exportGroupsAndRoles=true" \
-o "$BACKUP_DIR/realm-$REALM.json"
done
# 3. Compress backup
echo "=== Compressing backup ==="
tar -czf "$BACKUP_DIR.tar.gz" -C "$(dirname $BACKUP_DIR)" "$(basename $BACKUP_DIR)"
rm -rf "$BACKUP_DIR"
# 4. Upload to S3 (optional)
# aws s3 cp "$BACKUP_DIR.tar.gz" "s3://my-backups/keycloak/"
echo "=== Backup completed: $BACKUP_DIR.tar.gz ==="
#!/bin/bash
# restore-keycloak.sh - Restore from backup
BACKUP_FILE="$1"
if [ -z "$BACKUP_FILE" ]; then
echo "Usage: $0 "
exit 1
fi
# 1. Extract backup
RESTORE_DIR="/tmp/keycloak-restore"
mkdir -p "$RESTORE_DIR"
tar -xzf "$BACKUP_FILE" -C "$RESTORE_DIR"
# 2. Restore database
echo "=== Restoring database ==="
DB_DUMP=$(find "$RESTORE_DIR" -name "*.dump" | head -1)
pg_restore -h db-host -U keycloak -d keycloak \
--clean --if-exists \
"$DB_DUMP"
# 3. Restart Keycloak
echo "=== Restarting Keycloak ==="
# Docker Compose
docker compose restart keycloak
# Hoặc Kubernetes
# kubectl rollout restart deployment/keycloak -n keycloak
echo "=== Restore completed ==="
10.3 Failover Automation
#!/bin/bash
# failover-check.sh - Health check và auto-failover script
PRIMARY_URL="https://auth-primary.example.com/health/ready"
BACKUP_URL="https://auth-backup.example.com/health/ready"
DNS_ZONE="example.com"
DNS_RECORD="auth"
MAX_FAILURES=3
FAILURE_COUNT=0
CHECK_INTERVAL=10
while true; do
# Check primary health
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout 5 --max-time 10 \
"$PRIMARY_URL")
if [ "$HTTP_CODE" != "200" ]; then
FAILURE_COUNT=$((FAILURE_COUNT + 1))
echo "[$(date)] Primary UNHEALTHY ($HTTP_CODE) - failure $FAILURE_COUNT/$MAX_FAILURES"
if [ "$FAILURE_COUNT" -ge "$MAX_FAILURES" ]; then
echo "[$(date)] FAILOVER: Switching DNS to backup site"
# Check backup is healthy first
BACKUP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout 5 --max-time 10 \
"$BACKUP_URL")
if [ "$BACKUP_CODE" == "200" ]; then
# Update DNS (example with AWS Route53)
# aws route53 change-resource-record-sets ...
# Send alert
echo "CRITICAL: Keycloak failover executed at $(date)" | \
mail -s "Keycloak Failover Alert" [email protected]
FAILURE_COUNT=0
else
echo "[$(date)] CRITICAL: Both sites are down!"
fi
fi
else
if [ "$FAILURE_COUNT" -gt 0 ]; then
echo "[$(date)] Primary recovered"
fi
FAILURE_COUNT=0
fi
sleep "$CHECK_INTERVAL"
done
11. Docker Compose HA Cluster
Ví dụ hoàn chỉnh với 2 Keycloak nodes + External Infinispan + PostgreSQL:
# docker-compose-ha.yml
version: "3.9"
services:
# ============ PostgreSQL ============
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: db_password
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak"]
interval: 10s
timeout: 5s
retries: 5
networks:
- keycloak-net
# ============ External Infinispan ============
infinispan:
image: quay.io/infinispan/server:15.0
environment:
USER: keycloak
PASS: ispn_password
volumes:
- ./infinispan.xml:/opt/infinispan/server/conf/infinispan.xml
ports:
- "11222:11222"
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:11222/rest/v2/cache-managers/default/health/status"]
interval: 10s
timeout: 5s
retries: 10
networks:
- keycloak-net
# ============ Keycloak Node 1 ============
keycloak-1:
image: quay.io/keycloak/keycloak:26.0
command: start --optimized
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: db_password
KC_DB_POOL_MIN_SIZE: "10"
KC_DB_POOL_MAX_SIZE: "50"
KC_HOSTNAME: localhost
KC_PROXY_HEADERS: xforwarded
KC_HTTP_ENABLED: "true"
KC_HEALTH_ENABLED: "true"
KC_METRICS_ENABLED: "true"
KC_CACHE: ispn
KC_CACHE_STACK: kubernetes
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
JAVA_OPTS_APPEND: >-
-Djgroups.dns.query=keycloak-headless
-XX:+UseG1GC -XX:MaxRAMPercentage=70.0
depends_on:
postgres:
condition: service_healthy
infinispan:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8080/health/ready || exit 1"]
interval: 15s
timeout: 5s
retries: 10
start_period: 120s
networks:
keycloak-net:
aliases:
- keycloak-headless
# ============ Keycloak Node 2 ============
keycloak-2:
image: quay.io/keycloak/keycloak:26.0
command: start --optimized
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: db_password
KC_DB_POOL_MIN_SIZE: "10"
KC_DB_POOL_MAX_SIZE: "50"
KC_HOSTNAME: localhost
KC_PROXY_HEADERS: xforwarded
KC_HTTP_ENABLED: "true"
KC_HEALTH_ENABLED: "true"
KC_METRICS_ENABLED: "true"
KC_CACHE: ispn
KC_CACHE_STACK: kubernetes
JAVA_OPTS_APPEND: >-
-Djgroups.dns.query=keycloak-headless
-XX:+UseG1GC -XX:MaxRAMPercentage=70.0
depends_on:
postgres:
condition: service_healthy
infinispan:
condition: service_healthy
keycloak-1:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8080/health/ready || exit 1"]
interval: 15s
timeout: 5s
retries: 10
start_period: 120s
networks:
keycloak-net:
aliases:
- keycloak-headless
# ============ HAProxy Load Balancer ============
haproxy:
image: haproxy:2.9-alpine
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
ports:
- "8080:80"
- "8443:443"
- "8404:8404" # Stats
depends_on:
keycloak-1:
condition: service_healthy
keycloak-2:
condition: service_healthy
networks:
- keycloak-net
volumes:
pgdata:
networks:
keycloak-net:
driver: bridge
12. Infinispan CLI và Monitoring
# Infinispan CLI - kết nối đến server
bin/cli.sh -c https://infinispan-server:11222
# Xem cluster status
[infinispan-server:11222]> site status --all-caches
[infinispan-server:11222]> cache ls
[infinispan-server:11222]> cache info sessions
# Xem cache entries
[infinispan-server:11222]> cache entries sessions --limit 10
# Cache statistics
[infinispan-server:11222]> stats
# Xem cluster members
[infinispan-server:11222]> describe
# REST API monitoring
# Cluster health
curl -u admin:password \
"http://infinispan:11222/rest/v2/cache-managers/default/health"
# Cache statistics
curl -u admin:password \
"http://infinispan:11222/rest/v2/caches/sessions?action=stats"
# Cluster members
curl -u admin:password \
"http://infinispan:11222/rest/v2/cluster?action=distribution"