Chuyển đến nội dung chính

レッスン 24: 高可用性、クラスタリング、およびマルチサイト

Keycloak高可用性の概念、Infinispan分散キャッシュ(組み込み対外部)、キャッシュスタック構成(kubernetes、jdbc-ping、dns-ping)、セッションレプリケーション、外部Infinispanサーバーセットアップ、マルチサイト/クロスデータセンター展開、アクティブ/パッシブ対アクティブ/アクティブパターン、データベースレプリケーション(PostgreSQL Patroni、PgBouncer)、ロードバランサースティッキーセッション、スプリットブレイン処理、災害復旧戦略。

🔒 DevSecOps — レッスン 24 レッスン 24: 高可用性、クラスタリング、および マルチサイト

基本から上級までの Keycloak

パート 7: 本番環境、HA、および Kubernetes

xdev.asia

1. HA アーキテクチャの概要

Keycloak の高可用性は、ノードの認証/認可を容易に行うことができます。 HA は 3 つの柱に基づいています。クラスタリング (Infinispan), データベースのレプリケーション、 そして負荷分散.

┌──────────────────────────────────────────────────────────────────┐
│                    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 キャッシュの種類

2.1 ローカル キャッシュと分散/レプリケート キャッシュ

Keycloakは、異なる目的で2種類のInfinispanキャッシュを使用します。

キャッシュタイプ説明するキークロークのキャッシュクラスターモード
ローカルキャッシュ現在のノードにデータを保存し、DB から読み取れるメタデータに使用します。レルム, ユーザー。ユーザー, 認可。認可, キーローカル + 無効化メッセージ
分散キャッシュクラスター内の N 人の所有者に分散されたデータ (セッション データに使用される)セッション, 認証セッション, オフラインセッション, クライアントセッション, オフラインクライアントセッション分散型 (デフォルトでは 2 人の所有者)
複製されたキャッシュすべてのノードにデータがレプリケートされる仕事。仕事(クラスター通信)複製された

2.2 Keycloak キャッシュ アーキテクチャの詳細

┌─────────────────────────────────────────────────────────────────────┐
│                    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. 組み込み vs 外部 Infinispan

3.1 組み込み Infinispan (デフォルト)

デフォルトでは、KeycloakはInfinispanをJVMプロセスに埋め込みます。ノードは検出プロトコルを通じて相互に検出し、自動的にクラスターを形成します。

# Embedded Infinispan (default) - không cần cấu hình thêm
bin/kc.sh start --optimized \
  --cache=ispn

3.2 外部インフィニスパン

マルチサイト展開の場合、または個別のキャッシュ レイヤー管理が必要な場合は、外部 Infinispan サーバーを使用します。

# 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
特徴埋め込み型インフィニスパン外部インフィニスパン
セットアップの複雑さシンプルで構成不要Infinispan クラスターを個別にデプロイする必要がある
マルチサイト❌ サポートされていません✅ データセンター間のレプリケーション
スケーラビリティ10 ノード以下に適しています大規模な導入に適しています
管理自動Infinispan を個別に監視/管理する必要がある
ユースケース単一サイト、単一クラスターマルチサイト、DR、大規模展開

4. キャッシュスタックの構成

4.1 Kubernetes (KUBE_PING)

Kubernetes では、使用しますKUBE_PING(dns.DNS_PING) Keycloak ポッドが Kubernetes API または DNS 経由で相互に自己検出できるようにします。

# Keycloak trên Kubernetes - dùng dns cache stack
bin/kc.sh start --optimized \
  --cache=ispn \
  --cache-stack=kubernetes

Keycloak Kubernetes には Keycloak が必要ですヘッドレスサービスDNS 検出が機能するには:

# 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 (VM デプロイメント用)

VM (Kubernetes なし) にデプロイする場合は、次を使用します。JDBC_PING共有データベースを介してノードが相互に検出できるようにします。

<?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. 外部 Infinispan サーバーのセットアップ

5.1 Infinispanサーバーの構成

<!-- 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のリモートキャッシュ構成

外部 Infinispan に接続するように Keycloak を構成します。

<?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. マルチサイト展開

6.1 アクティブ/パッシブパターン

アクティブ-パッシブでは、のみプライマリサイトトラフィックに対応します。スタンバイ状態のバックアップ サイト。プライマリに問題が発生した場合に引き継ぐ準備ができています。

┌──────────────────────────────────────────────────────────────────────┐
│                  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 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%)             │
└──────────────────────────────────────────────────────────────────────┘
特徴アクティブ-パッシブアクティブ-アクティブ
交通処理一度に 1 サイト両方のサイトを同時に
リソースの使用率50% (パッシブサイトのアイドル状態)100%
複雑より低いより高い (競合解決)
フェイルオーバー時間DNS スイッチが必要 (秒-分)自動 (GSLB)
データの一貫性強力 (同期リプル)最終的 (非同期クロスサイト)
DB要件プライマリ-スタンバイマルチマスターまたは共有DB

6.3 データセンター間の Infinispan 構成

<!-- 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. データベースのレプリケーション

7.1 PostgreSQL Patroni + PgBouncer

パトローニPostgreSQL HA (自動フェイルオーバー) 管理、Pgバウンサー接続プーリングを提供します。

┌─────────────────────────────────────────────────────────────┐
│            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.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 の設定

# 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. ロードバランサの構成

8.1 スティッキーセッション

キークロークスティッキーセッションが必要です認証フロー (ログイン、登録) 用。セッション アフィニティは Cookie ベースですKC_ROUTE(前にKEYCLOAK_SESSION):

8.2 HAProxy 構成

# 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 ロードバランサ

# 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. スプリットブレインの処理

9.1 スプリット ブレイン シナリオ

スプリット ブレインは、ネットワーク パーティションによってクラスターが 2 つ以上のパーティションに分割され、各パーティションがもう一方のパーティションが停止していると認識するときに発生します。

┌─────────────────────────────────────────────────────────────┐
│                    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 マージポリシー

<!-- 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>
マージポリシー行動使用事例
PREFERRED_NON_NULLマージ時にエントリを null 以外に保つセッション - セッションを失うよりは維持したほうがよい
REMOVE_ALL競合するエントリをすべて削除しますデータの一貫性が最も重要な場合
優先_常により大きなパーティションからのエントリを保持する汎用

10. 災害復旧戦略

10.1 RPO/RTO 目標

階層RPORTO戦略
レベル 1 (重大)0 (データ損失ゼロ)<; 5分アクティブ-アクティブ + 同期 DB レプリケーション
レベル 2 (重要)<; 1分<; 15分アクティブ/パッシブ + 非同期レプリケーション + 自動フェイルオーバー
階層 3 (標準)<; 1時間<; 1時間バックアップ/リストア + 手動フェイルオーバー

10.2 バックアップと復元

#!/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 フェイルオーバーの自動化

#!/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 クラスター

2 つの Keycloak ノード + 外部 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 とモニタリング

# 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"