LabHub

블로그

Keycloak 쿠버네티스 HA 운영 — Infinispan 클러스터링과 무중단 배포

한국어English日本語中文

들어가며

SSO 서버는 조직의 모든 서비스가 의존하는 단일 진입점입니다. Keycloak이 죽으면 로그인이 멈추고, 로그인이 멈추면 사실상 전사 장애입니다. 그래서 Keycloak 운영의 핵심 과제는 언제나 고가용성(HA)이었습니다.

다행히 2026년의 Keycloak 26.x는 HA 운영의 난이도를 크게 낮췄습니다. persistent user sessions가 기본값이 되면서 "재기동하면 전 사용자 로그아웃" 문제가 사라졌고, 26.6의 zero-downtime rolling patch로 패치 업그레이드 시 무중단 배포가 공식 지원됩니다. 이 글에서는 쿠버네티스 위에서 Keycloak HA 클러스터를 구축하고 운영하는 전 과정을 다룹니다.

배포 방식 선택 — Operator vs Helm

쿠버네티스에 Keycloak을 올리는 대표적인 방법은 공식 Operator와 커뮤니티 Helm 차트(주로 Bitnami 또는 codecentric)입니다.

항목Keycloak Operator (공식)Helm 차트 (커뮤니티)
유지보수 주체Keycloak 프로젝트 공식커뮤니티/벤더
추상화 수준Keycloak CR로 선언values.yaml로 세부 제어
26.6 rolling patch 자동화지원 (update strategy)수동 구성 필요
realm importKeycloakRealmImport CR초기화 스크립트
커스텀 이미지지원 (권장 패턴)지원
세밀한 파드 제어제한적 (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 |
                     | (sessions   |      | service      |
                     |  영속화)     |      | (DNS_PING)   |
                     +-------------+      +--------------+

distributed 캐시는 기본적으로 owners 수가 2여서, 하나의 엔트리가 두 노드에 복제됩니다. 즉 노드 하나가 죽어도 세션 데이터는 살아 있습니다. 동시에 두 노드를 잃으면 캐시 상의 데이터는 유실될 수 있지만, 26부터는 세션이 DB에도 영속화되므로 복구가 가능합니다.

JGroups DNS_PING — 쿠버네티스에서의 노드 디스커버리

Infinispan의 클러스터 멤버십은 JGroups가 담당합니다. 쿠버네티스에서는 멀티캐스트가 막혀 있으므로 DNS 기반 디스커버리(DNS_PING)를 사용합니다. 동작 원리는 단순합니다.

  1. headless Service가 모든 Keycloak 파드의 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를 켜는 이유는, 파드가 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 기능이 기본 활성화되어 다음이 바뀌었습니다.

대가는 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 기준 필수는 아니지만 여전히 유익합니다.

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 확대는 실무 필수 팁입니다. 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 /        |
               |     health-check 기반 failover) |
               +---------------------------------+

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. 캐시/설정 호환이면 파드를 하나씩 교체 (무중단)
  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 쓰기 포함
메모리(비힙 포함)파드당 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분의 기동 유예

추가로 PodDisruptionBudget과 topologySpreadConstraints는 HA의 기본기입니다.

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

장애 시나리오별 대응

시나리오 1: 노드 1대 다운

시나리오 2: DB 순단 (failover 30초)

시나리오 3: 스플릿 브레인 (네트워크 파티션)

시나리오 4: 전체 재기동 (재해 복구)

# 정기 realm export (CronJob으로 자동화 권장)
bin/kc.sh export --dir /tmp/export --realm production --users different_files

시나리오 5: 로그인 폭주 (출근 시간 스파이크)

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)을 다룹니다.

참고 자료

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다