Skip to content
Published on

Keycloak Kubernetes HA 運用 — Infinispan クラスタリングと無停止デプロイ

Authors

はじめに

SSO サーバーは組織のすべてのサービスが依存する単一の入り口です。Keycloak が落ちればログインが止まり、ログインが止まれば事実上の全社障害です。だからこそ Keycloak 運用の中心課題は常に高可用性(HA)でした。

幸い、2026 年の Keycloak 26.x は HA 運用の難易度を大きく下げました。persistent user sessions がデフォルトになったことで「再起動すると全ユーザーがログアウト」という問題が消え、26.6 の zero-downtime rolling patch によりパッチアップグレード時の無停止デプロイが公式にサポートされます。本記事では Kubernetes 上で Keycloak HA クラスタを構築・運用する全工程を扱います。

  • Operator vs Helm のデプロイ方式の選択
  • Infinispan キャッシュ構造と JGroups DNS_PING ディスカバリ
  • persistent user sessions の意味と動作
  • DB 選択、コネクションプール、sticky session 論争の整理
  • multi-site(cross-DC)Active-Active 構成
  • リソースサイジング、JVM チューニング、ヘルスチェック
  • 障害シナリオ別の対応マニュアル

デプロイ方式の選択 — Operator vs Helm

Kubernetes に Keycloak を載せる代表的な方法は、公式 Operator とコミュニティ Helm チャート(主に Bitnami または codecentric)です。

項目Keycloak Operator (公式)Helm チャート (コミュニティ)
メンテナンス主体Keycloak プロジェクト公式コミュニティ/ベンダー
抽象化レベルKeycloak CR で宣言values.yaml で詳細制御
26.6 rolling patch 自動化サポート (update strategy)手動構成が必要
realm importKeycloakRealmImport CR初期化スクリプト
カスタムイメージサポート (推奨パターン)サポート
細かい Pod 制御限定的 (podTemplate で補完)自由
推奨対象標準構成、運用自動化重視非標準トポロジ、既存 Helm パイプライン

新規構築なら公式 Operator を推奨します。バージョンアップグレードの自動化と 26.6 の無停止パッチ戦略が Operator に組み込まれているからです。Operator のインストールは次のとおりです。

kubectl create namespace keycloak
kubectl apply -n keycloak \
  -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.2/kubernetes/keycloaks.k8s.keycloak.org-v1.yml
kubectl apply -n keycloak \
  -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.2/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml
kubectl apply -n keycloak \
  -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.6.2/kubernetes/kubernetes.yml

Keycloak CR の実践例です。

apiVersion: k8s.keycloak.org/v2alpha1
kind: Keycloak
metadata:
  name: keycloak
  namespace: keycloak
spec:
  instances: 3
  image: registry.example.com/idp/keycloak-custom:26.6.2
  startOptimized: true
  db:
    vendor: postgres
    host: keycloak-db.database.svc.cluster.local
    port: 5432
    database: keycloak
    usernameSecret:
      name: keycloak-db-secret
      key: username
    passwordSecret:
      name: keycloak-db-secret
      key: password
    poolMinSize: 10
    poolInitialSize: 10
    poolMaxSize: 30
  hostname:
    hostname: sso.example.com
    strict: true
  http:
    httpEnabled: true
  proxy:
    headers: xforwarded
  additionalOptions:
    - name: log-console-output
      value: json
    - name: event-metrics-user-enabled
      value: "true"
  resources:
    requests:
      cpu: "1"
      memory: 1500Mi
    limits:
      memory: 3Gi
  update:
    strategy: Auto

Infinispan キャッシュ構造 — セッションと認証状態の保管庫

Keycloak クラスタの心臓部は組み込みの Infinispan キャッシュです。ノード間の状態共有はすべてここで行われます。主要キャッシュを分類すると次のとおりです。

キャッシュ名種類用途26+ のデフォルト動作
realms, userslocalDB エンティティの読み取りキャッシュノードごとにローカル、invalidation メッセージで同期
authorizationlocal認可ポリシーキャッシュノードごとにローカル
sessions, clientSessionsdistributedログインセッションDB 永続化 + キャッシュ
offlineSessionsdistributedオフラインセッションDB 永続化 + キャッシュ
authenticationSessionsdistributed進行中の認証 (ログインフォーム段階)クラスタ分散
loginFailuresdistributedブルートフォースカウンタクラスタ分散
workreplicatedノード間 invalidation の伝播全ノード複製
actionTokensdistributedメールリンクなどワンタイムトークンクラスタ分散

構造を図で見ると次のようになります。

        +-----------------+   +-----------------+   +-----------------+
        |  Keycloak Pod 1 |   |  Keycloak Pod 2 |   |  Keycloak Pod 3 |
        |                 |   |                 |   |                 |
        | local: realms,  |   | local: realms,  |   | local: realms,  |
        |        users    |   |        users    |   |        users    |
        |                 |   |                 |   |                 |
        | distributed:    |   | distributed:    |   | distributed:    |
        |  sessions(o2)  <----> sessions(o2)   <----> sessions(o2)    |
        |  authSessions   |   |  authSessions   |   |  authSessions   |
        |                 |   |                 |   |                 |
        | replicated:     |   | replicated:     |   | replicated:     |
        |  work          <----> work           <----> work            |
        +--------+--------+   +--------+--------+   +--------+--------+
                 |                     |                     |
                 +----------+----------+----------+----------+
                            |   JGroups (gossip)  |
                            v                     v
                     +-------------+      +--------------+
                     | PostgreSQL  |      | DNS headless |
                     | (セッション  |      | service      |
                     |  永続化)     |      | (DNS_PING)   |
                     +-------------+      +--------------+

distributed キャッシュはデフォルトで owners 数が 2 で、一つのエントリが二つのノードに複製されます。つまりノードが一台落ちてもセッションデータは生きています。同時に二台失うとキャッシュ上のデータは失われる可能性がありますが、26 からはセッションが DB にも永続化されるため復旧が可能です。

JGroups DNS_PING — Kubernetes でのノードディスカバリ

Infinispan のクラスタメンバーシップは JGroups が担当します。Kubernetes ではマルチキャストが塞がれているため、DNS ベースのディスカバリ(DNS_PING)を使用します。動作原理はシンプルです。

  1. headless Service がすべての Keycloak Pod の IP を DNS A レコードとして公開
  2. 各ノードが起動時にその DNS 名を照会してピア一覧を取得
  3. JGroups が 7800 ポートでクラスタを形成

Operator を使えば自動構成されますが、手動で構成する場合は次のとおりです。

apiVersion: v1
kind: Service
metadata:
  name: keycloak-discovery
  namespace: keycloak
spec:
  clusterIP: None
  publishNotReadyAddresses: true
  selector:
    app: keycloak
  ports:
    - name: jgroups
      port: 7800
      targetPort: 7800
# Keycloak 起動オプション (StatefulSet/Deployment の環境変数として)
KC_CACHE=ispn
KC_CACHE_STACK=kubernetes
JAVA_OPTS_APPEND=-Djgroups.dns.query=keycloak-discovery.keycloak.svc.cluster.local

publishNotReadyAddresses を有効にする理由は、Pod が readiness 前の段階でもクラスタに参加しなければ、起動中のセッションリバランシングが正常に動作しないためです。クラスタ形成の確認はログで行います。

kubectl logs -n keycloak keycloak-0 | grep "ISPN000094"
# ISPN000094: Received new cluster view ... (3) [keycloak-0-..., keycloak-1-..., keycloak-2-...]

26.x からは JGroups トラフィックの TLS 暗号化がデフォルトで有効になり(Operator デプロイ時)、ノード間のセッションデータが平文で流れることはありません。

Persistent User Sessions — 26 のゲームチェンジャー

Keycloak 24 までオンラインセッションは純粋なインメモリ(Infinispan)でした。全体再起動や複数ノードの同時障害ですべてのユーザーがログアウトされる構造でした。Keycloak 26 から persistent-user-sessions 機能がデフォルトで有効になり、次のことが変わりました。

  • すべての user session / client session が作成時点で DB に記録されます。
  • Infinispan はホットデータキャッシュの役割に降格し、真実の源泉は DB になります。
  • クラスタ全体の再起動後もユーザーはログイン状態を維持します。
  • メモリ使用量が大きく減ります(セッション全体をメモリに保持する必要がない)。

代償は DB 書き込み負荷の増加です。ログイン/ログアウト/refresh のたびに DB 書き込みが発生するため、ログイン集中シナリオ(朝 9 時の出勤時間)の DB IOPS を見積もりに反映する必要があります。無効化は可能ですが(features から除外)、26 の運用モデルは永続セッションを前提に設計されているため、特別な理由がなければデフォルトの維持を推奨します。

DB 選択とコネクションプール

項目推奨理由
DB エンジンPostgreSQL 15+公式パフォーマンステストの基準、Aurora PostgreSQL 検証済み
分離レベルREAD COMMITTEDデフォルト、変更不要
プールサイズノードあたり max 30 前後過大なプールは DB 負荷を増やすだけ
HAPatroni / RDS Multi-AZ / AuroraDB が SPOF にならないように
プール見積もりピーク同時リクエスト基準ログイン TPS x 平均クエリ数を考慮

コネクションプールの公式は単純ではありませんが、経験則として「ログイン 100 TPS あたりノードごとにプール 10-15」から始め、モニタリング(agroal メトリクス)で調整します。プールが枯渇するとログイン遅延ではなくログイン失敗に直結するため、db-pool 関連メトリクスにアラートを設定すべきです。

# Keycloak CR 内のコネクションプール設定部分
  db:
    poolMinSize: 10
    poolInitialSize: 10
    poolMaxSize: 30
  additionalOptions:
    - name: transaction-xa-enabled
      value: "false"

Sticky Session は必要か

結論から: 26 基準で必須ではありませんが、依然として有益です。

  • authenticationSessions(ログイン進行状態)は distributed キャッシュなので、どのノードにリクエストが行っても処理可能です。
  • ただし同じノードに継続的にルーティングされると owner ノード直行の確率が高まり、ノード間 RPC が減って遅延が改善します。
  • Keycloak は AUTH_SESSION_ID クッキーにノード情報をエンコードしており、これを活用する LB(例: ingress-nginx の session affinity)は自然に sticky 動作をします。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: keycloak
  namespace: keycloak
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "KC_ROUTE"
    nginx.ingress.kubernetes.io/proxy-buffer-size: "128k"
spec:
  ingressClassName: nginx
  rules:
    - host: sso.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: keycloak-service
                port:
                  number: 8080
  tls:
    - hosts: [sso.example.com]
      secretName: sso-tls

proxy-buffer-size の拡大は実務必須の Tips です。Keycloak のレスポンスヘッダー(特にトークンを含むリダイレクト)がデフォルトバッファを超えて 502 が発生するケースがよくあります。

Multi-Site (Cross-DC) Active-Active

26.x の公式 multi-site アーキテクチャは二サイトの Active-Active をサポートします。中核となる構成要素は次のとおりです。

        Site A (eu-west-1)                  Site B (eu-central-1)
   +------------------------+         +------------------------+
   |  Keycloak (3 pods)     |         |  Keycloak (3 pods)     |
   |        |               |         |        |               |
   |  Infinispan (external) | <-----> |  Infinispan (external) |
   |  cross-site replication|  RELAY2 |  cross-site replication|
   +-----------+------------+         +-----------+------------+
               |                                  |
               +----------------+-----------------+
                                |
                     +----------v-----------+
                     |  Aurora Global DB    |
                     |  (writer: Site A)    |
                     +----------------------+
                                ^
               +----------------+----------------+
               |     Global LB (Route53 /        |
               |     ヘルスチェックベース failover)|
               +---------------------------------+
  • セッション同期: 外部 Infinispan クラスタの cross-site replication(RELAY2)
  • DB: Aurora Global Database のような単一 writer のグローバル DB
  • ルーティング: グローバル LB がヘルスチェックに基づき二サイトにトラフィックを分配
  • persistent user sessions のおかげで、サイト間キャッシュ同期が失敗しても DB 経由での復旧が可能

multi-site は運用の複雑さが非常に高いため、RTO/RPO 要件が本当に必要な場合にのみ導入し、その前に単一リージョン複数 AZ + 堅牢なバックアップ/リストア手順で十分かをまず検討するのがよいでしょう。詳細は公式 HA ガイドを参照してください。

26.6 Zero-Downtime Rolling Patch

26.6 以前は、どのバージョンアップグレードでもキャッシュプロトコル非互換の可能性のため、クラスタ全体を落として上げ直す recreate 戦略が基本でした。26.6 からはパッチリリース間(例: 26.6.0 から 26.6.2)の互換性が保証され、rolling update が公式サポートされます。

# Keycloak CR
spec:
  update:
    strategy: Auto   # 互換性を自動判定: 可能なら rolling、不可なら recreate

Auto 戦略の動作は次のとおりです。

  1. Operator が新しいイメージで update-compatibility 検査ジョブを実行
  2. キャッシュ/設定が互換なら Pod を一台ずつ入れ替え(無停止)
  3. 非互換なら recreate(全体再起動、persistent sessions でログインは維持)

手動で互換性を検査することもできます。

# 既存バージョンでメタデータを生成
bin/kc.sh update-compatibility metadata --file=/tmp/metadata.json

# 新バージョンで検査
bin/kc.sh update-compatibility check --file=/tmp/metadata.json
echo $?   # 0 なら rolling 可能

リソースサイジングと JVM チューニング

公式サイジングガイドに基づく出発点は次のとおりです。

負荷指標1 vCPU あたりの処理量(概算)備考
パスワードログイン毎秒 15 回前後ハッシュコスト(argon2)に大きく左右
client credentials グラント毎秒 120 回前後最も軽い処理
refresh token毎秒 120 回前後DB 書き込みを含む
メモリ(非ヒープ含む)Pod あたり 1.25-3Giセッション数より realm/クライアント数の影響大

JVM メモリは 26 からコンテナメモリベースの比率算定がデフォルトです(デフォルトヒープ 70%)。明示的に制御するには:

  additionalOptions: []
  # または環境変数で
  # JAVA_OPTS_KC_HEAP: "-XX:MaxRAMPercentage=70 -XX:InitialRAMPercentage=50"
  resources:
    requests:
      cpu: "1"
      memory: 1500Mi
    limits:
      memory: 3Gi

CPU limit は設定しないのが一般的な推奨です(スロットリングによる遅延スパイクの防止)。メモリ limit は OOMKill 防止のため、ヒープ+メタスペース+ネイティブの合算より余裕を持たせます。

ヘルスチェックと Startup Probe

Keycloak は管理ポート(デフォルト 9000)で health エンドポイントを提供します。

# Deployment/StatefulSet を直接構成する場合
livenessProbe:
  httpGet:
    path: /health/live
    port: 9000
  periodSeconds: 10
  failureThreshold: 3
readinessProbe:
  httpGet:
    path: /health/ready
    port: 9000
  periodSeconds: 10
  failureThreshold: 3
startupProbe:
  httpGet:
    path: /health/started
    port: 9000
  periodSeconds: 5
  failureThreshold: 60   # 最大 5 分の起動猶予
  • started: 起動完了の判定。startup probe 専用で、マイグレーションが長いアップグレード直後を考慮し failureThreshold に余裕を持たせます。
  • ready: DB 接続可否を含みます。DB の瞬断時に Pod が一斉に not-ready になり全面障害のように見える点を知っておくべきです。
  • live: プロセス自体の生存。失敗時は再起動が発生するため保守的に設定します。

さらに PodDisruptionBudget と topologySpreadConstraints は HA の基本です。

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: keycloak-pdb
  namespace: keycloak
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: keycloak

障害シナリオ別の対応

シナリオ 1: ノード 1 台ダウン

  • 症状: ほぼなし。distributed キャッシュ owners 2 でセッション保持、LB がトラフィックを再分配。
  • 対応: Pod の自動再作成を確認。JGroups クラスタビューに再参加したかログで確認。

シナリオ 2: DB 瞬断 (failover 30 秒)

  • 症状: 全ノードの readiness 失敗、ログイン/トークン発行が全面失敗。既発行トークンの検証への影響は少ない(署名検証はローカル)。
  • 対応: DB failover の自動化確認が最優先。Keycloak は DB 復帰時に自動回復するため Pod の再起動は不要。むしろ liveness を敏感にしすぎて再起動の嵐にならないよう注意。

シナリオ 3: スプリットブレイン (ネットワークパーティション)

  • 症状: クラスタが二グループに分かれ、それぞれビューを形成。ブルートフォースカウンタ/セッションの不整合の可能性。
  • 対応: 26 のデフォルト設定はパーティション統合時に MERGE イベントで回復。persistent sessions のおかげでセッションデータは DB 基準に収束。パーティションが頻発するなら CNI/ノードネットワークの点検が根本対応。

シナリオ 4: 全体再起動 (災害復旧)

  • 症状: 26 以前は全ユーザーログアウトでしたが、26+ では DB からセッションが復元されログインを維持。
  • 対応: DB バックアップから復元する最悪シナリオに備え、realm export を別途保管(設定とデータの二重バックアップ)。
# 定期 realm export (CronJob での自動化を推奨)
bin/kc.sh export --dir /tmp/export --realm production --users different_files

シナリオ 5: ログイン集中 (出勤時間スパイク)

  • 症状: CPU 飽和、パスワードハッシュ演算がボトルネック。
  • 対応: HPA で水平スケールしますが、ハッシュコストが CPU を支配するためスケールアウトが直接的に効果あり。ただし DB コネクション総数が DB の上限を超えないようプール最大値を再計算。
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: keycloak-hpa
  namespace: keycloak
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: StatefulSet
    name: keycloak
  minReplicas: 3
  maxReplicas: 8
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

おわりに

Keycloak 26 時代の HA 運用は「Infinispan をなんとかなだめる作業」から「DB を中心に据えたごく普通のステートフルサービス運用」へとシンプルになりました。persistent user sessions と zero-downtime rolling patch がその転換点です。それでも JGroups ディスカバリ、コネクションプール見積もり、プローブチューニングといった基本は依然として運用者の仕事です。本記事の YAML 例を出発点として、必ず自分の環境の負荷テストで数値を検証してください。

次の記事では、Keycloak の機能そのものを拡張する SPI 開発(カスタム Authenticator、EventListener)を扱います。

参考資料