업로드한 문서와 메모리 정보를 바탕으로 50,000 RPS를 지원하는 무중단 Keycloak 시스템 구축 가이드
1만 명 사용자 기준, LDAP 연동 및 SSO 기능을 포함한다.
목표: 엔터프라이즈급 인증 시스템 구축 (50,000 RPS, 1만 명 사용자)
주요 요구사항:
도메인: iam.lake.com
[외부 시스템] [Kubernetes 클러스터]
┌──────────────┐ ┌─────────────────────────────┐
│ LDAP │────────────────│ Keycloak User Federation │
│ Server │ LDAPS │ (LDAP 동기화) │
└──────────────┘ └─────────────────────────────┘
│
┌──────────────┐ │
│ SAML IdP │────────────────┐ │
│ (외부) │ SAML 2.0 │ │
└──────────────┘ │ │
│ │
▼ ▼
┌──────────────┐ ┌─────────────────────────┐
│ Client │────────▶│ L4 Load Balancer │
│ Apps │ HTTPS │ (Session Affinity) │
└──────────────┘ └─────────────────────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Ingress │ │ Ingress │ │ Ingress │
│ (Worker1) │ │ (Worker2) │ │ (Worker3) │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Keycloak │◄──│ Keycloak │──▶│ Keycloak │
│ Pod 1 │ │ Pod 2 │ │ Pod 3 │
│(Infinispan│ │(Infinispan│ │(Infinispan│
│ Cluster) │ │ Cluster) │ │ Cluster) │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└────────────────┼────────────────┘
│ JDBC Connection Pool
│
┌────────────────┼────────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│PostgreSQL │ │PostgreSQL │ │PostgreSQL │
│ Primary │──▶│ Replica 1 │ │ Replica 2 │
│ (CNPG) │ │ (CNPG) │ │ (CNPG) │
│ │ │ │ │ │
│ + PVC │ │ + PVC │ │ + PVC │
│ (NVMe) │ │ (NVMe) │ │ (NVMe) │
└───────────┘ └───────────┘ └───────────┘
Control Plane (Master Nodes):
Worker Nodes (Keycloak 전용):
이유:
# NVMe 로컬 스토리지 (최고 성능)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-nvme
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
---
# 백업용 블록 스토리지
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: backup-storage
provisioner: kubernetes.io/aws-ebs # 또는 환경에 맞는 provisioner
parameters:
type: io2
iopsPerGB: "50"
encrypted: "true"
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
스토리지 전략:
Primary Storage (로컬 NVMe):
Backup Storage (블록 스토리지):
이유:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: keycloak-postgres
namespace: iam-system
spec:
instances: 3
# 리소스 할당
resources:
requests:
cpu: "16"
memory: "64Gi"
limits:
cpu: "32"
memory: "128Gi"
# 고성능 스토리지
storage:
storageClass: local-nvme
size: 500Gi
# PostgreSQL 설정
postgresql:
parameters:
max_connections: "500"
shared_buffers: "16GB"
effective_cache_size: "48GB"
maintenance_work_mem: "2GB"
checkpoint_completion_target: "0.9"
wal_buffers: "16MB"
default_statistics_target: "100"
random_page_cost: "1.1"
effective_io_concurrency: "200"
work_mem: "32MB"
min_wal_size: "2GB"
max_wal_size: "8GB"
max_worker_processes: "16"
max_parallel_workers_per_gather: "4"
max_parallel_workers: "16"
max_parallel_maintenance_workers: "4"
# Replication 설정
wal_level: "replica"
max_wal_senders: "10"
max_replication_slots: "10"
hot_standby: "on"
# 성능 모니터링
shared_preload_libraries: "pg_stat_statements"
pg_stat_statements.track: "all"
# Primary 설정
primaryUpdateStrategy: unsupervised
# Backup 설정
backup:
barmanObjectStore:
destinationPath: s3://keycloak-backup/
s3Credentials:
accessKeyId:
name: backup-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: backup-creds
key: ACCESS_SECRET_KEY
wal:
compression: gzip
encryption: AES256
maxParallel: 4
retentionPolicy: "30d"
# 백업 스케줄
scheduledBackup:
- name: daily-backup
schedule: "0 2 * * *" # 매일 새벽 2시
backupOwnerReference: self
# 모니터링
monitoring:
enablePodMonitor: true
# 안티어피니티 (노드 분산)
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
cnpg.io/cluster: keycloak-postgres
topologyKey: kubernetes.io/hostname
PostgreSQL 튜닝 설명:
Connection Pool:
max_connections: 500 (Keycloak Pod당 100 connection × 3 pods + 여유)메모리 튜닝:
shared_buffers: 16GB (총 메모리의 25%)effective_cache_size: 48GB (총 메모리의 75%)work_mem: 32MB (정렬/해시 작업용)WAL 최적화:
wal_buffers: 16MB (대량 트랜잭션 처리)checkpoint_completion_target: 0.9 (I/O 분산)병렬 처리:
max_parallel_workers: 16 (복잡한 쿼리 병렬화)이유:
random_page_cost 조정# keycloak-values.yaml
global:
domain: iam.lake.com
replicas: 5 # 50,000 RPS 처리를 위한 충분한 replica
image:
repository: quay.io/keycloak/keycloak
tag: "26.3.3"
# 리소스 할당 (성능 테스트 결과 기반)
resources:
requests:
cpu: "32"
memory: "96Gi"
limits:
cpu: "80"
memory: "160Gi"
# JVM 설정
extraEnv:
- name: JAVA_OPTS_APPEND
value: >-
-XX:+UseG1GC
-XX:MaxRAMPercentage=70.0
-XX:+UseStringDeduplication
-XX:+ParallelRefProcEnabled
-XX:MaxGCPauseMillis=100
-Djgroups.dns.query=keycloak-headless.iam-system.svc.cluster.local
# Keycloak 설정
command:
- "/opt/keycloak/bin/kc.sh"
- "start"
- "--optimized"
extraEnvFrom:
- configMapRef:
name: keycloak-config
- secretRef:
name: keycloak-secret
# 데이터베이스 연결
dbchecker:
enabled: true
postgresql:
enabled: false # 외부 CNPG 사용
externalDatabase:
host: keycloak-postgres-rw.iam-system.svc.cluster.local
port: 5432
database: keycloak
user: keycloak
existingSecret: keycloak-db-secret
existingSecretPasswordKey: password
# Infinispan 클러스터링
cache:
stack: kubernetes
# 서비스 설정
service:
type: ClusterIP
httpPort: 8080
httpsPort: 8443
# Ingress 설정
ingress:
enabled: true
ingressClassName: nginx
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
rules:
- host: iam.lake.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- iam.lake.com
secretName: keycloak-tls
# 고가용성 설정
podDisruptionBudget:
enabled: true
minAvailable: 3
# 안티어피니티 (노드 분산 배치)
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app.kubernetes.io/name: keycloak
topologyKey: kubernetes.io/hostname
# Health Check
livenessProbe:
httpGet:
path: /health/live
port: 8080
scheme: HTTP
initialDelaySeconds: 120
periodSeconds: 30
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /health/ready
port: 8080
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
# 메트릭 수집
metrics:
enabled: true
serviceMonitor:
enabled: true
apiVersion: v1
kind: ConfigMap
metadata:
name: keycloak-config
namespace: iam-system
data:
# 기본 설정
KC_HOSTNAME: "iam.lake.com"
KC_HOSTNAME_STRICT: "false"
KC_HOSTNAME_STRICT_HTTPS: "true"
KC_HTTP_ENABLED: "true"
KC_PROXY: "edge"
KC_HEALTH_ENABLED: "true"
KC_METRICS_ENABLED: "true"
# 데이터베이스 설정
KC_DB: "postgres"
KC_DB_URL_HOST: "keycloak-postgres-rw.iam-system.svc.cluster.local"
KC_DB_URL_PORT: "5432"
KC_DB_URL_DATABASE: "keycloak"
KC_DB_USERNAME: "keycloak"
KC_DB_SCHEMA: "public"
# Connection Pool 튜닝 (50,000 RPS 대응)
KC_DB_POOL_INITIAL_SIZE: "20"
KC_DB_POOL_MIN_SIZE: "20"
KC_DB_POOL_MAX_SIZE: "100"
# HTTP 스레드 풀 튜닝
KC_HTTP_POOL_MAX_THREADS: "330"
# 캐시 설정
KC_CACHE: "ispn"
KC_CACHE_STACK: "kubernetes"
# 로깅
KC_LOG_LEVEL: "INFO"
KC_LOG_CONSOLE_OUTPUT: "json"
# 기능 활성화
KC_FEATURES: "token-exchange,admin-fine-grained-authz,declarative-user-profile"
설정 근거:
Pod 리소스:
DB Connection Pool:
max_connections: 500과 일치KC_DB_POOL_MAX_SIZE: 100HTTP 스레드:
KC_HTTP_POOL_MAX_THREADS: 330Keycloak은 내장 Infinispan을 사용하며, Pod 간 JGroups로 클러스터링한다.
캐시 유형별 전략:
Local Cache (노드별 독립):
realms: Realm 메타데이터 (변경 거의 없음)users: 사용자 정보 (LDAP 동기화 데이터)authorization: 권한 정보keys: 공개키 캐시Distributed Cache (샤딩 방식):
sessions: 사용자 세션 (Owners=1)authenticationSessions: 인증 진행 중 세션 (Owners=2)clientSessions: 클라이언트별 세션loginFailures: 로그인 실패 기록 (Owners=2)Replicated Cache (전체 복제):
work: 클러스터 작업 큐 (모든 노드가 즉시 공유 필요)JGroups 설정:
기본적으로 JDBC_PING을 사용하지만, Kubernetes 환경에서는 DNS_PING 권장:
# Keycloak ConfigMap에 추가
KC_CACHE_CONFIG_FILE: "cache-ispn-jdbc-ping.xml"
커스텀 Infinispan 설정 (필요 시):
<!-- /opt/keycloak/conf/cache-ispn.xml -->
<infinispan xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:infinispan:config:14.0 http://www.infinispan.org/schemas/infinispan-config-14.0.xsd"
xmlns="urn:infinispan:config:14.0">
<cache-container name="keycloak">
<transport stack="kubernetes" />
<!-- Session 캐시: 대용량 분산 처리 -->
<distributed-cache name="sessions"
owners="1"
mode="ASYNC">
<memory max-count="100000"
when-full="REMOVE"/>
<expiration lifespan="-1"
max-idle="-1"/>
</distributed-cache>
<!-- Realm 캐시: 로컬에서 빠르게 조회 -->
<local-cache name="realms">
<memory max-count="10000"/>
<expiration lifespan="-1"/>
</local-cache>
<!-- 기타 캐시도 동일한 패턴으로 설정 -->
</cache-container>
<jgroups>
<stack name="kubernetes" extends="tcp">
<dns.DNS_PING
dns_query="keycloak-headless.iam-system.svc.cluster.local"
dns_record_type="A"
stack.combine="REPLACE"
stack.position="MPING"/>
</stack>
</jgroups>
</infinispan>
이유:
Keycloak Admin Console에서 설정하거나 Terraform/Script로 자동화 가능.
LDAP 구성 예시 (Admin Console 기준):
User Federation 생성:
User Federation > Add provider > ldapLDAP 연결 설정:
# LDAP Secret (Kubernetes)
apiVersion: v1
kind: Secret
metadata:
name: ldap-credentials
namespace: iam-system
type: Opaque
stringData:
bindDN: "cn=keycloak-service,ou=services,dc=example,dc=com"
bindPassword: "your-secure-password"
Keycloak LDAP Provider 설정:
| 파라미터 | 값 | 설명 |
|---|---|---|
| Connection URL | ldaps://ldap.example.com:636 | LDAP 서버 (보안 연결) |
| Bind Type | simple | 인증 방식 |
| Bind DN | cn=keycloak-service,ou=services,dc=example,dc=com | 서비스 계정 |
| Bind Credential | ${ldap-password} | 비밀번호 (Secret 참조) |
| Users DN | ou=users,dc=example,dc=com | 사용자 검색 Base DN |
| Username LDAP attribute | uid | 사용자명 매핑 속성 |
| RDN LDAP attribute | uid | RDN 속성 |
| UUID LDAP attribute | entryUUID | 고유 ID |
| User Object Classes | inetOrgPerson, organizationalPerson | 사용자 objectClass |
| Connection Pooling | true | 연결 풀 사용 |
| Pagination | true | 대량 조회 시 페이징 |
| Edit Mode | READ_ONLY | LDAP 읽기 전용 (Keycloak에서 수정 불가) |
| Sync Registrations | false | Keycloak 신규 등록 시 LDAP 동기화 안 함 |
| Import Users | true | 로그인 시 자동 Import |
| Batch Size | 1000 | 동기화 배치 크기 |
Attribute Mapper 설정 (LDAP → Keycloak):
| Mapper 이름 | LDAP Attribute | Keycloak Attribute | 설명 |
|---|---|---|---|
| 이메일 매핑 | |||
| firstName | givenName | firstName | 이름 |
| lastName | sn | lastName | 성 |
| username | uid | username | 사용자명 |
| employeeNumber | employeeNumber | employee_id | 사번 (커스텀 속성) |
Group Mapper 설정:
| 파라미터 | 값 |
|---|---|
| Groups DN | ou=groups,dc=example,dc=com |
| Group Object Classes | groupOfNames |
| Membership Attribute | member |
| Mode | READ_ONLY |
Periodic Full Sync (정기 전체 동기화):
Periodic Full Sync: Enabled, Full Sync Period: 86400 (1일)Periodic Changed Users Sync (변경된 사용자만):
Periodic Changed Users Sync: Enabled, Changed Users Sync Period: 3600이유:
Identity Provider 추가:
# Realm JSON 설정 예시 (Terraform으로 관리 권장)
{
"realm": "master",
"identityProviders": [
{
"alias": "external-saml-idp",
"providerId": "saml",
"enabled": true,
"config": {
"singleSignOnServiceUrl": "https://external-idp.example.com/sso/saml",
"singleLogoutServiceUrl": "https://external-idp.example.com/slo/saml",
"validateSignature": "true",
"signingCertificate": "${file:external-idp-cert.pem}",
"nameIDPolicyFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
"principalType": "SUBJECT",
"postBindingResponse": "true",
"postBindingAuthnRequest": "true",
"wantAuthnRequestsSigned": "true"
},
"mappers": [
{
"name": "email",
"identityProviderMapper": "saml-user-attribute-idp-mapper",
"config": {
"attribute.name": "email",
"user.attribute": "email"
}
}
]
}
]
}
SAML 연동 흐름:
1. 사용자가 iam.lake.com 접속
2. Keycloak이 외부 SAML IdP로 리다이렉트
3. 외부 IdP에서 인증 후 SAML Response 전송
4. Keycloak이 SAML 검증 및 사용자 매핑
5. 애플리케이션에 Keycloak 토큰 발급
Client 설정 예시:
# Application 1: React SPA
{
"clientId": "react-app",
"enabled": true,
"protocol": "openid-connect",
"publicClient": true,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"redirectUris": [
"https://app1.lake.com/*"
],
"webOrigins": [
"https://app1.lake.com"
],
"attributes": {
"pkce.code.challenge.method": "S256"
}
}
# Application 2: Backend API (Confidential)
{
"clientId": "backend-api",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"serviceAccountsEnabled": true,
"authorizationServicesEnabled": true,
"redirectUris": [
"https://api.lake.com/auth/callback"
],
"secret": "** Generated Secret **"
}
SSO 세션 관리:
| 설정 | 값 | 설명 |
|---|---|---|
| SSO Session Idle | 30분 | 비활성 시 로그아웃 |
| SSO Session Max | 10시간 | 최대 세션 유지 시간 |
| Access Token Lifespan | 5분 | 토큰 유효기간 (짧게) |
| Refresh Token Max | 30분 | Refresh Token 최대 수명 |
이유:
# AWS NLB 예시 (Terraform)
resource "aws_lb" "keycloak" {
name = "keycloak-nlb"
internal = false
load_balancer_type = "network"
subnets = var.public_subnet_ids
enable_cross_zone_load_balancing = true
tags = {
Environment = "production"
Service = "keycloak"
}
}
resource "aws_lb_target_group" "keycloak_https" {
name = "keycloak-https-tg"
port = 443
protocol = "TCP"
vpc_id = var.vpc_id
health_check {
enabled = true
protocol = "HTTPS"
path = "/health/ready"
port = "traffic-port"
healthy_threshold = 3
unhealthy_threshold = 3
timeout = 10
interval = 30
}
stickiness {
enabled = true
type = "source_ip"
}
deregistration_delay = 30
}
resource "aws_lb_listener" "keycloak_https" {
load_balancer_arn = aws_lb.keycloak.arn
port = "443"
protocol = "TCP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.keycloak_https.arn
}
}
L4 튜닝 포인트:
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-ingress-controller
namespace: ingress-nginx
data:
# 연결 타임아웃
proxy-connect-timeout: "75"
proxy-send-timeout: "600"
proxy-read-timeout: "600"
# 버퍼 크기
proxy-buffer-size: "16k"
proxy-buffers: "4 16k"
# 업로드 크기
proxy-body-size: "10m"
# 클라이언트 연결
client-header-buffer-size: "64k"
large-client-header-buffers: "4 64k"
# 성능 튜닝
worker-processes: "auto"
max-worker-connections: "65536"
# SSL 최적화
ssl-protocols: "TLSv1.2 TLSv1.3"
ssl-ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384"
ssl-session-cache: "shared:SSL:10m"
ssl-session-timeout: "10m"
# 로그 최적화
access-log-path: /dev/stdout
error-log-path: /dev/stderr
log-format-upstream: '{"timestamp":"$time_iso8601","remote_addr":"$remote_addr","request_method":"$request_method","request_uri":"$request_uri","status":$status,"request_time":$request_time,"upstream_response_time":"$upstream_response_time"}'
Ingress 리소스 할당:
resources:
requests:
cpu: "4"
memory: "8Gi"
limits:
cpu: "8"
memory: "16Gi"
# cert-manager ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@lake.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
ServiceMonitor 설정:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: keycloak
namespace: iam-system
labels:
app: keycloak
spec:
selector:
matchLabels:
app.kubernetes.io/name: keycloak
endpoints:
- port: http
path: /metrics
interval: 30s
scrapeTimeout: 10s
주요 메트릭:
| 메트릭 | 설명 | 알림 임계값 |
|---|---|---|
keycloak_login_attempts_total | 로그인 시도 수 | - |
keycloak_login_errors_total | 로그인 실패 수 | 분당 100건 이상 |
keycloak_response_time_seconds | 응답 시간 | P95 > 1초 |
keycloak_active_sessions | 활성 세션 수 | > 80,000 |
keycloak_db_pool_active | DB 연결 사용 수 | > 90 (전체 100 중) |
keycloak_cache_hits_total | 캐시 히트 수 | - |
keycloak_cache_misses_total | 캐시 미스 수 | 히트율 < 70% |
PostgreSQL 메트릭:
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
name: postgres-keycloak
namespace: iam-system
spec:
selector:
matchLabels:
cnpg.io/cluster: keycloak-postgres
podMetricsEndpoints:
- port: metrics
path: /metrics
| 메트릭 | 설명 | 알림 임계값 |
|---|---|---|
pg_stat_database_xact_commit | 커밋된 트랜잭션 | - |
pg_stat_database_conflicts | 복제 충돌 수 | > 0 |
pg_replication_lag_seconds | Replication Lag | > 5초 |
pg_database_size_bytes | DB 크기 | > 400GB (전체 500GB 중 80%) |
pg_stat_activity_count | 활성 연결 수 | > 450 (전체 500 중 90%) |
Keycloak 대시보드 JSON (주요 패널):
{
"dashboard": {
"title": "Keycloak IAM Monitoring",
"panels": [
{
"title": "RPS (Requests Per Second)",
"targets": [
{
"expr": "rate(keycloak_http_requests_total[5m])"
}
]
},
{
"title": "Login Success Rate",
"targets": [
{
"expr": "rate(keycloak_login_attempts_total{result='success'}[5m]) / rate(keycloak_login_attempts_total[5m]) * 100"
}
]
},
{
"title": "Cache Hit Rate",
"targets": [
{
"expr": "keycloak_cache_hits_total / (keycloak_cache_hits_total + keycloak_cache_misses_total) * 100"
}
]
},
{
"title": "Response Time P95",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(keycloak_response_time_seconds_bucket[5m]))"
}
]
}
]
}
}
알림 규칙:
# PrometheusRule
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: keycloak-alerts
namespace: iam-system
spec:
groups:
- name: keycloak
interval: 30s
rules:
- alert: KeacloakHighErrorRate
expr: rate(keycloak_login_errors_total[5m]) > 100
for: 5m
labels:
severity: warning
annotations:
summary: "Keycloak 로그인 실패율 급증"
description: "최근 5분간 분당 {{ $value }} 건의 로그인 실패"
- alert: KeacloakSlowResponse
expr: histogram_quantile(0.95, rate(keycloak_response_time_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "Keycloak 응답 속도 저하"
description: "P95 응답 시간: {{ $value }}초"
- alert: PostgreSQLReplicationLag
expr: pg_replication_lag_seconds > 5
for: 2m
labels:
severity: critical
annotations:
summary: "PostgreSQL Replication Lag 발생"
description: "Lag: {{ $value }}초"
- alert: KeacloakPodDown
expr: kube_deployment_status_replicas_available{deployment="keycloak"} < 3
for: 1m
labels:
severity: critical
annotations:
summary: "Keycloak Pod 개수 부족"
description: "현재 {{ $value }}개만 실행 중 (최소 3개 필요)"
# 1. Namespace 생성
kubectl create namespace iam-system
# 2. Secret 생성
kubectl create secret generic keycloak-db-secret \
--from-literal=password='your-db-password' \
-n iam-system
kubectl create secret generic ldap-credentials \
--from-literal=bindDN='cn=keycloak-service,ou=services,dc=example,dc=com' \
--from-literal=bindPassword='ldap-password' \
-n iam-system
# 3. CNPG Operator 설치
kubectl apply -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.22/releases/cnpg-1.22.0.yaml
# 4. PostgreSQL 클러스터 배포
kubectl apply -f cnpg-cluster.yaml -n iam-system
# 5. PostgreSQL 초기화 대기
kubectl wait --for=condition=Ready cluster/keycloak-postgres -n iam-system --timeout=300s
# 6. Keycloak 배포 (Helm)
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install keycloak bitnami/keycloak \
-f keycloak-values.yaml \
-n iam-system
# 7. 배포 확인
kubectl get pods -n iam-system
kubectl get ingress -n iam-system
# Admin 비밀번호 확인
kubectl get secret keycloak -n iam-system -o jsonpath='{.data.admin-password}' | base64 -d
# Admin Console 접속
echo "https://iam.lake.com/admin"
Terraform 자동화 예시:
# main.tf
terraform {
required_providers {
keycloak = {
source = "mrparkers/keycloak"
version = "~> 4.0"
}
}
}
provider "keycloak" {
client_id = "admin-cli"
username = "admin"
password = var.keycloak_admin_password
url = "https://iam.lake.com"
initial_login = false
}
# Realm 생성
resource "keycloak_realm" "main" {
realm = "lake-company"
enabled = true
display_name = "Lake Company IAM"
display_name_html = "<b>Lake Company</b>"
# 세션 설정
sso_session_idle_timeout = "30m"
sso_session_max_lifespan = "10h"
# 토큰 설정
access_token_lifespan = "5m"
# 로그인 설정
login_with_email_allowed = true
registration_allowed = false
}
# LDAP Federation
resource "keycloak_ldap_user_federation" "ldap" {
realm_id = keycloak_realm.main.id
name = "corporate-ldap"
enabled = true
connection_url = "ldaps://ldap.example.com:636"
bind_dn = "cn=keycloak-service,ou=services,dc=example,dc=com"
bind_credential = var.ldap_bind_password
users_dn = "ou=users,dc=example,dc=com"
username_ldap_attribute = "uid"
rdn_ldap_attribute = "uid"
uuid_ldap_attribute = "entryUUID"
user_object_classes = ["inetOrgPerson", "organizationalPerson"]
edit_mode = "READ_ONLY"
import_enabled = true
sync_registrations = false
# 동기화 설정
full_sync_period = 86400 # 1일
changed_sync_period = 3600 # 1시간
}
# Client 생성 (React SPA 예시)
resource "keycloak_openid_client" "react_app" {
realm_id = keycloak_realm.main.id
client_id = "react-app"
enabled = true
access_type = "public"
standard_flow_enabled = true
implicit_flow_enabled = false
direct_access_grants_enabled = false
valid_redirect_uris = [
"https://app1.lake.com/*"
]
web_origins = [
"https://app1.lake.com"
]
# PKCE 활성화
pkce_code_challenge_method = "S256"
}
# Terraform 실행
terraform init
terraform plan
terraform apply
# RollingUpdate 전략
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
# Helm 업그레이드
helm upgrade keycloak bitnami/keycloak \
-f keycloak-values.yaml \
-n iam-system \
--wait
# 롤백 (문제 발생 시)
helm rollback keycloak -n iam-system
문제: Introspect API는 Keycloak 서버 호출이 필요하여 성능 병목
해결: 로컬 JWT 검증 사용
애플리케이션 구현 (Node.js 예시):
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
// JWKS 클라이언트 설정 (공개키 캐싱)
const client = jwksClient({
jwksUri: 'https://iam.lake.com/realms/lake-company/protocol/openid-connect/certs',
cache: true,
cacheMaxAge: 86400000, // 24시간
rateLimit: true,
jwksRequestsPerMinute: 10
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
// 토큰 검증 미들웨어
function verifyToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
// 로컬에서 JWT 검증 (Keycloak 호출 없음!)
jwt.verify(token, getKey, {
audience: 'react-app',
issuer: 'https://iam.lake.com/realms/lake-company',
algorithms: ['RS256']
}, (err, decoded) => {
if (err) {
return res.status(401).json({ error: 'Invalid token' });
}
req.user = decoded;
next();
});
}
효과:
Keycloak → PostgreSQL:
# ConfigMap에 추가
KC_DB_POOL_INITIAL_SIZE: "20"
KC_DB_POOL_MIN_SIZE: "20"
KC_DB_POOL_MAX_SIZE: "100"
애플리케이션 → Keycloak (토큰 발급 시):
// HTTP Connection Pool
const axios = require('axios');
const https = require('https');
const agent = new https.Agent({
keepAlive: true,
maxSockets: 50,
maxFreeSockets: 10,
timeout: 60000,
scheduling: 'lifo'
});
const keycloakClient = axios.create({
httpsAgent: agent,
baseURL: 'https://iam.lake.com'
});
초기 부팅 시 주요 데이터를 미리 캐시에 로드:
# Keycloak 시작 후 실행하는 스크립트
#!/bin/bash
KEYCLOAK_URL="https://iam.lake.com"
REALM="lake-company"
# Realm 메타데이터 조회 (캐시에 로드)
curl -s "${KEYCLOAK_URL}/realms/${REALM}/.well-known/openid-configuration" > /dev/null
# 공개키 조회 (캐시에 로드)
curl -s "${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/certs" > /dev/null
echo "Cache warming completed"
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: keycloak-policy
namespace: iam-system
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: keycloak
policyTypes:
- Ingress
- Egress
ingress:
# Ingress Controller만 허용
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 8080
- protocol: TCP
port: 8443
egress:
# PostgreSQL만 허용
- to:
- podSelector:
matchLabels:
cnpg.io/cluster: keycloak-postgres
ports:
- protocol: TCP
port: 5432
# DNS 허용
- to:
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: UDP
port: 53
# LDAP 서버 허용
- to:
- ipBlock:
cidr: 10.0.0.0/8 # LDAP 서버 IP 대역
ports:
- protocol: TCP
port: 636
# Keycloak ServiceAccount에 최소 권한만 부여
apiVersion: v1
kind: ServiceAccount
metadata:
name: keycloak
namespace: iam-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: keycloak-role
namespace: iam-system
rules:
- apiGroups: [""]
resources: ["endpoints", "services"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: keycloak-rolebinding
namespace: iam-system
subjects:
- kind: ServiceAccount
name: keycloak
namespace: iam-system
roleRef:
kind: Role
name: keycloak-role
apiGroup: rbac.authorization.k8s.io
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: keycloak-psp
spec:
privileged: false
allowPrivilegeEscalation: false
requiredDropCapabilities:
- ALL
volumes:
- 'configMap'
- 'emptyDir'
- 'projected'
- 'secret'
- 'downwardAPI'
- 'persistentVolumeClaim'
hostNetwork: false
hostIPC: false
hostPID: false
runAsUser:
rule: 'MustRunAsNonRoot'
seLinux:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'
readOnlyRootFilesystem: false
PostgreSQL 백업:
# CNPG ScheduledBackup
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: keycloak-backup
namespace: iam-system
spec:
schedule: "0 2 * * *" # 매일 새벽 2시
backupOwnerReference: self
cluster:
name: keycloak-postgres
수동 백업:
# On-demand 백업
kubectl cnpg backup keycloak-postgres -n iam-system
PostgreSQL 복구:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: keycloak-postgres-restored
spec:
instances: 3
bootstrap:
recovery:
source: keycloak-postgres
recoveryTarget:
targetTime: "2026-01-20 12:00:00" # 특정 시점 복구
externalClusters:
- name: keycloak-postgres
barmanObjectStore:
destinationPath: s3://keycloak-backup/
s3Credentials:
accessKeyId:
name: backup-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: backup-creds
key: ACCESS_SECRET_KEY
kubectl get pods -n iam-system)| 항목 | 사양 | 수량 | 월 비용 (USD) |
|---|---|---|---|
| EKS Control Plane | - | 1 | $73 |
| Worker Node (r6i.32xlarge) | 128C/1024GB | 3 | $9,072 |
| NLB | - | 1 | $30 |
| EBS (io2) | 500GB, 50 IOPS/GB | 3 | $1,650 |
| S3 (백업) | 2TB | 1 | $46 |
| 데이터 전송 | 10TB/월 | - | $900 |
| 합계 | $11,771 |
비용 최적화 옵션:
이 가이드는 Keycloak 26.3.3 기반, 50,000 RPS를 지원하는 엔터프라이즈급 인증 시스템을 구축하는 완전한 청사진이다.
핵심 포인트:
다음 단계:
문서의 모든 설정은 실제 프로덕션 사례와 성능 테스트 결과를 바탕으로 작성했다. 필요 시 각 섹션을 더 상세히 확장할 수 있다.