1. KeyCloak 이란 무엇인가요?
keycloak은 계정관리 및 access 관리를 제공합니다. Single-sign-on이 가능한 오픈소스입니다. application을 구축하고자 할 때 코드의 변경없이 인증과 자원보호의 기능을 제공하는 솔루션입니다. 해당 솔루션은 오픈소스로 제공되며 community 버전의 경우 별도의 비용없이 사용이 가능합니다. 개발자가 작성해야하는 보안 기능을 기본적으로 제공하며, 조직의 개별 요구 사항에 맞게 쉽게 조정가능합니다. keycloak은 로그인, 등록, 관리 및 계정관리를 위한 사용자 정의 가능한 인터페이스를 제공합니다.2. 설치 방법
필자는 Keycloak 설치 자체를 공식 헬름 차트를 이용하여 설치했습니다. 해당 차트를 보고 필요한 부분만을 취득하여 가져왔습니다. 헬름 패키지 매니저를 사용하여 쿠버네티스 클러스터에 StatefulSet으로 설치를 진행합니다. 기본적으로 해당 차트는 PostgreSQL 차트를 의존하고 있습니다. 또한 설치시 인프라 스트럭처 안에서 PV 지원을 요구합니다. 필자는 MySQL로 진행하기 때문에 해당 부분을 변경하여 배포 하였습니다. 2026년 9월 27일 기준으로 다시 확인한 내용 차트 저장소를 다시 열어 보니 이 글에서 참고한 codecentric/keycloak 차트는 예전 WildFly 배포판용이라 더 이상 유지보수되지 않는다는 경고가 README 맨 위에 붙어 있었습니다. 마지막 버전은 18.10.0(appVersion 17.0.1-legacy)이고, 지금 새로 설치하신다면 같은 저장소의 keycloakx 차트로 가시는 것이 맞는 것 같습니다. 아래 표는 2021년 이 글을 쓸 때와 지금 값을 나란히 적은 것입니다.| 항목 | 이 글(2021년) | 2026년 9월 기준 |
|---|---|---|
| 헬름 차트 | codecentric/keycloak | codecentric/keycloakx 7.3.2 (2026-09-17) |
| Keycloak 버전 | 15.0.2 (WildFly 배포판) | 26.7.4 (Quarkus 배포판, 2026-09-16) |
| 컨테이너 이미지 | docker.io/jboss/keycloak | quay.io/keycloak/keycloak |
| 관리자 계정 환경변수 | KEYCLOAK_USER, KEYCLOAK_PASSWORD | KC_BOOTSTRAP_ADMIN_USERNAME, KC_BOOTSTRAP_ADMIN_PASSWORD |
| 관리 포트 | 9990 | 9000 (/metrics, /health) |
| HA 노드 탐색 | JGroups dns.DNS_PING + 헤드리스 서비스 | jdbc-ping (DB 로 노드 탐색, 차트 기본값) |
| 프로메테우스 메트릭 | keycloak-metrics-spi 2.5.3 jar | 내장 옵션 –metrics-enabled=true |
helm repo add codecentric https://codecentric.github.io/helm-charts
helm repo update
helm install keycloak codecentric/keycloakx --values ./values.yaml
keycloakx 차트는 values 를 비워 두면 h2 파일 DB 로 뜨기 때문에 운영에는 맞지 않고, 필자처럼 MySQL 이나 PostgreSQL 을 따로 붙여야 합니다. 아래 매니페스트들은 2021년 당시 WildFly 배포판 기준으로 직접 구성했던 기록이라 그대로 남겨 두었습니다.
KeyCloak 공식 Document
KeyCloak 공식 헬름차트
configmap
apiVersion: v1
kind: ConfigMap
metadata:
name: configmap
data:
keycloak.cli: |
embed-server --server-config=standalone-ha.xml --std-out=echo
batch
echo Configuring node identifier
## Sets the node identifier to the node name (= pod name). Node identifiers have to be unique. They can have a
## maximum length of 23 characters. Thus, the chart's fullname template truncates its length accordingly.
/subsystem=transactions:write-attribute(name=node-identifier, value=${jboss.node.name})
echo Finished configuring node identifier
run-batch
stop-embedded-server
해당 부분은 KeyCloak HA 구성시 중요한 부분입니다. 예를들어 replica > 1 이상인 경우에, 특정 KeyCloak application 으로 Session이 맺어진 상황에서 다시 DB로의 연결이 이루어질 때, 데이터베이스 트랜잭션을 관리하기 위해 각각 별도의 고유한 노드 식별자가 필요합니다.
즉, POD에 대한 Identifier가 필요한데, 이는 POD의 이름으로 할 수 있습니다. POD이름을 사용할 경우, jboss의 변수 길이 제한이 23자로 제한이므로, StatefulSet을 사용하는 것이 좋습니다.
secrets
apiVersion: v1
kind: Secret
metadata:
name: db
labels:
type: Opaque
data:
DB_PASSWORD: {{ .Values.dbPassword }}
DB_USER: {{ .Values.dbUser }}
해당 부분은 KeyCloak의 DB 연결시 base64로 encoding한 값을 secret이라는 kubernetes object를 통해서 mount 시켜주어, env속성값을 사용할 수 있게 구성했습니다.
Service-headless
apiVersion: v1
kind: Service
metadata:
name: headless-service
spec:
type: ClusterIP
clusterIP: None
ports:
- name: http
port: {{ .Values.httpPort }}
targetPort: http
protocol: TCP
selector:
service.phase: {{ .Values.phase }}
service.name: {{ .Values.namespace }}
project.name: {{ .Values.projectName }}
KeyCloak이 HA 구성이 됐을 경우, 내부적으로는 Unique함을 인지해야합니다. 이를 위해서는 멀티캐스트 방식을 이용하는데 kubernetes에 설치된 CNI에 따라서 지원이 안되는 경우가 있습니다. 필자같은 경우, Calico Network을 사용하고있어, 별도의 멀티 캐스트 방식을 구성해야했습니다.
KeyCloak은 이를 해결하기 위해, JGroups라는 것을 이용합니다. JGroups를 사용하기 위한 옵션이 존재하는데, 기본적으로 헬름차트에서 제공해주는 dns.PING방식을 사용했습니다.
Headless service(Cluster IP가 없는)를 만들어 POD에 연결을 해줍니다. 그리고 이 헤드리스 Kube DNS를 통해서 JGroup 멀티 캐스트 방식을 가능하도록 구성했습니다.
Service-Nodeport
apiVersion: v1
kind: Service
metadata:
name: service-NodePort
spec:
type: NodePort
ports:
- name: http
port: {{ .Values.httpPort }}
targetPort: http
protocol: TCP
nodePort: {{ .Values.httpNodePort }}
- name: https
port: {{ .Values.httpsPort }}
targetPort: https
nodePort: {{ .Values.httpsNodePort }}
protocol: TCP
selector:
service.phase: {{ .Values.phase }}
service.name: {{ .Values.namespace }}
project.name: {{ .Values.projectName }}
Service는 단순하게 외부로 노출시킬 포트들에 한해서 NodePort로 구성을 했습니다. 앞단에 Ingress를 붙이려고했으나, 아직 테스트가 미비하여 NodePort로 구성을 하고 LB를 연결하였습니다.
Statefulsets
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: Statefulsets
labels:
service.phase: {{ .Values.phase }}
service.name: {{ .Values.namespace }}
project.name: {{ .Values.projectName }}
spec:
selector:
matchLabels:
service.phase: {{ .Values.phase }}
service.name: {{ .Values.namespace }}
project.name: {{ .Values.projectName }}
replicas: {{ .Values.replicas }}
serviceName: headless-service
podManagementPolicy: Parallel
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
service.phase: {{ .Values.phase }}
service.name: {{ .Values.namespace }}
project.name: {{ .Values.projectName }}
annotations:
sidecar.istio.io/inject: {{ .Values.istioInject | quote }}
sidecar.istio.io/gracefulshutdown: {{ .Values.istioGracefulshutdown | quote }}
sidecar.istio.io/rewriteAppHTTPProbers: {{ .Values.istioRewriteAppHTTPProbers | quote }}
prometheus.io/scrape: {{ .Values.prometheusScrape | quote }}
prometheus.io/port: {{ .Values.prometheusPort | quote }}
prometheus.io/path: {{ .Values.prometheusPath | quote }}
spec:
containers:
- name: keycloak
securityContext:
runAsNonRoot: true
runAsUser: 1000
image: {{ .Values.dockerImgName }}:{{ .Values.deployImgVersion }}
imagePullPolicy: IfNotPresent
env:
- name: KEYCLOAK_USER
value: admin
- name: KEYCLOAK_PASSWORD
value: admin
- name: PROXY_ADDRESS_FORWARDING
value: "true"
- name: DB_VENDOR
value: mysql
- name: DB_ADDR
value: {{ .Values.dbAddress }}
- name: DB_DATABASE
value: {{ .Values.dbDatabase }}
- name: DB_PORT
value: {{ .Values.dbPort | quote }}
- name: DB_USER
valueFrom:
secretKeyRef:
name: '{{ .Values.phase }}-{{ .Values.projectName }}-db'
key: DB_USER
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: '{{ .Values.phase }}-{{ .Values.projectName }}-db'
key: DB_PASSWORD
- name: JGROUPS_DISCOVERY_PROTOCOL
value: dns.DNS_PING
- name: JGROUPS_DISCOVERY_PROPERTIES
value: 'dns_query={{ .Values.phase }}-{{ .Values.projectName }}-headless.{{ .Values.namespace }}.svc.cluster.local'
- name: CACHE_OWNERS_COUNT
value: "2"
- name: CACHE_OWNERS_AUTH_SESSIONS_COUNT
value: "2"
- name: JAVA_OPTS
value: -XX:+UseContainerSupport
-XX:MaxRAMPercentage=50.0
-Djava.net.preferIPv4Stack=true
-Djboss.modules.system.pkgs=$JBOSS_MODULES_SYSTEM_PKGS
-Djava.awt.headless=true
envFrom:
- secretRef:
name: '{{ .Values.phase }}-{{ .Values.projectName }}-db'
ports:
- name: http
containerPort: {{ .Values.httpPort }}
protocol: TCP
- name: https
containerPort: {{ .Values.httpsPort }}
protocol: TCP
- name: http-management
containerPort: 9990
protocol: TCP
- name: jgroup
containerPort: 7600
protocol: TCP
livenessProbe:
httpGet:
path: /auth/
port: http
initialDelaySeconds: 0
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /auth/realms/master
port: http
initialDelaySeconds: 30
timeoutSeconds: 1
startupProbe:
httpGet:
path: /auth/
port: http
initialDelaySeconds: 30
timeoutSeconds: 1
failureThreshold: 60
periodSeconds: 5
volumeMounts:
- name: startup
mountPath: "/opt/jboss/startup-scripts/keycloak.cli"
subPath: "keycloak.cli"
readOnly: true
securityContext:
fsGroup: 1000
enableServiceLinks: true
restartPolicy: Always
terminationGracePeriodSeconds: 60
volumes:
- name: startup
configMap:
name: {{ .Values.phase }}-{{ .Values.projectName }}-configmap
defaultMode: 0555
items:
- key: keycloak.cli
path: keycloak.cli
내용이 길지만, 주요한 부분에 대해서만 기록을 해두겠습니다.
- name: DB_VENDOR
value: mysql
- name: DB_ADDR
value: {{ .Values.dbAddress }}
- name: DB_DATABASE
value: {{ .Values.dbDatabase }}
- name: DB_PORT
value: {{ .Values.dbPort | quote }}
- name: DB_USER
valueFrom:
secretKeyRef:
name: '{{ .Values.phase }}-{{ .Values.projectName }}-db'
key: DB_USER
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: '{{ .Values.phase }}-{{ .Values.projectName }}-db'
key: DB_PASSWORD
DB 에 대한 정보들을 env로 mount한 내용입니다. 추가로 DB에 대한 계정과 패스워드 부분은 은 secret을 통해서 가져오도록 변경해두었습니다.
- name: JGROUPS_DISCOVERY_PROTOCOL
value: dns.DNS_PING
- name: JGROUPS_DISCOVERY_PROPERTIES
value: 'dns_query={{ .Values.phase }}-{{ .Values.projectName }}-headless.{{ .Values.namespace }}.svc.cluster.local'
headless-service를 통해서 JGroups 멀티캐스팅을 진행할 수 있도록 적용하는 옵션입니다. 실제로 해당 옵션이 빠트리고 HA구성을 시도하였는데 로그인이 되자 마자 바로 팅겨버리는 효과(?)가 있었습니다.
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: https
containerPort: 8443
protocol: TCP
- name: http-management
containerPort: 9990
protocol: TCP
- name: jgroup
containerPort: 7600
protocol: TCP
정말 시간을 많이 허비한 부분인데… 공식 헬름차트에는 80포트로 Container를 올리도록 구성이 되어있는데, 실제로는 8080포트를 사용합니다. 9990같은 경우는 내부적으로 설치되어있는 미들웨어인 Wildfly에 대한 포트입니다. 7600은 JGroups에 해당하는 포트로 Keycloak 설정파일에서 변경이 가능합니다.
volumeMounts:
- name: startup
mountPath: "/opt/jboss/startup-scripts/keycloak.cli"
subPath: "keycloak.cli"
readOnly: true
.........
volumes:
- name: startup
configMap:
name: {{ .Values.phase }}-{{ .Values.projectName }}-configmap
defaultMode: 0555
items:
- key: keycloak.cli
path: keycloak.cli
상위에서 설명했던 configmap을 볼륨으로 마운트 시켰고, 해당 내용을 바로 실행할 수 있는 공간에 넣어두었습니다.
3. 로그인 화면 변경
KeyCloak은 별도의 인증기능이 있어, 로그인 화면을 제공해주고있습니다. 그러나 기본 로그인 화면에 KeyCloak이라는 표기가 들어가고 이를 변경할 필요가 있었습니다. KeyCloak 같은 경우는 다음 경로에 theme 구성을 하고 있습니다.# keycloak themes 구성
/opt/jboss/keycloak/themes
# themes/keyclaok 하위 폴더
account/
admin/
common/
email/
login/
welcome/
위에 보시는것처럼 keyclaok 기본 테마 하위에 account부터 welcome까지 모든 페이지들에 대해서 custom이 가능하도록 되어있습니다. 보통은 로그인 화면 페이지를 많이 커스텀하여 사용하는것으로 나와있습니다.
keycloak custom login theme
필자는 위의 로그인 테마를 입혀서 사용했습니다.
FROM docker.io/jboss/keycloak:15.0.2
COPY ./login /opt/jboss/keycloak/themes/custom/login
Dockerfile에서 github에서 다운로드 받은 login theme 폴더를 그대로 themes 하위에 custom/login이라는 공간으로 복사를 해주도록 구성했습니다.
Keycloak 17 부터 들어온 Quarkus 배포판은 설치 경로가 /opt/jboss/keycloak 에서 /opt/keycloak 으로 바뀌었는데요, 지금 이미지로 빌드하신다면 FROM 을 quay.io/keycloak/keycloak:26.7.4 로 두고 테마 폴더를 /opt/keycloak/themes/custom 아래로 복사하시면 됩니다.
빌드를 수행한 후, docker registry에 push를 한 후, 배포를 수행합니다.
이후에, KeyCloak Realm > Themes > Login Theme > custom으로 설정후 Save를 누릅니다.
로그인 화면이 정상적으로 바뀐것을 볼 수 있습니다.
4. 프로메테우스 메트릭 설정
KeyCloak은 2가지 방식의 메트릭을 제공합니다.- /metrics -> Wildfly에서 제공하는 메트릭
- /auth/realms/master/metrics -> Keycloak에서 제공하는 메트릭
FROM docker.io/jboss/keycloak:15.0.2
COPY ./login /opt/jboss/keycloak/themes/custom/login
RUN sed -i 's/inet-address value="\${jboss.bind.address.management:127.0.0.1}"/any-address/g' /opt/jboss/keycloak/standalone/configuration/standalone-ha.xml
COPY ./keycloak-metrics-spi-2.5.3.jar /opt/jboss/keycloak/standalone/deployments/keycloak-metrics-spi-2.5.3.jar
4번째 줄인 keyclaok-metrics-spi.jar 파일을 keycloak deployments 폴더에 copy를 수행해줍니다.
이후, Realm > Events > Config > Events Config > metrics-listener 추가 후, Save 버튼을 누릅니다.
정해진 nodeport를 통해서 접속하면 정상적으로 데이터를 가져오는 것을 확인 할 수 있습니다.
2026년 기준으로는 이 jar 없이도 같은 결과를 얻을 수 있습니다. Keycloak 26 은 –metrics-enabled=true 로 메트릭을 켜고 –event-metrics-user-enabled=true 를 더하면 로그인 같은 사용자 이벤트 메트릭까지 관리 포트 9000 의 /metrics 로 내보냅니다. keycloakx 차트로 올리면 관리 경로가 컨텍스트 경로를 따라가서 /auth/metrics 가 됩니다. 참고로 keycloak-metrics-spi 는 7.0.0(2024-11-07)이 마지막 릴리스입니다.
5. 마치며…
이번시간에는 인증/인가를 대신해주는 SSO 어플리케이션인 KeyCloak에 대해서 알아봤습니다. 다음시간에는 KeyCloak의 사용법에 대해서도 간단히 정리를 해보도록 하겠습니다.Bitnami 이미지 없이 Keycloak 헬름 차트를 쓰려면?
Bitnami 공개 카탈로그는 2025년 8월 28일부터 무료 범위를 latest 태그만 있는 일부 이미지로 줄였고, 기존 이미지와 버전 태그는docker.io/bitnamilegacy 로 옮겼습니다. 운영에서 버전 태그를 계속 받으려면 유료 Bitnami Secure Images 를 쓰라고 안내합니다(bitnami/containers 공지).
그래서 먼저 지금 쓰는 차트가 어떤 이미지를 당기는지 봅니다.
helm template keycloak <차트 이름> | grep 'image:'
docker.io/bitnami/... 가 보이면 둘 중 하나로 옮기는 편이 낫습니다.
- codecentric keycloakx 차트: 기본 이미지가 Keycloak 공식
quay.io/keycloak/keycloak입니다.helm repo add codecentric https://codecentric.github.io/helm-charts뒤helm install keycloak codecentric/keycloakx로 설치합니다(차트 README). 아래 자주 묻는 질문의 /auth 경로 설명도 이 차트 기준입니다. - Keycloak Operator: 헬름 대신 Keycloak 프로젝트가 내는 Operator 로 Keycloak 리소스를 만들어 설치합니다(Keycloak Operator 설치 문서).
6. KeyCloak 헬름차트 자주 묻는 질문
codecentric keycloak 차트를 지금도 써도 되나요?
새로 설치하는 용도라면 권하지 않습니다. 이 차트는 WildFly 배포판용이고 README 에 더 이상 유지보수하지 않는다고 적혀 있으며, 마지막 버전 18.10.0 의 앱 버전이 17.0.1-legacy 입니다. 2026년 9월 기준으로는 같은 저장소의 keycloakx 7.3.2 가 Keycloak 26.7.4 를 올립니다.keycloakx 차트로 옮기면 /auth 경로는 어떻게 되나요?
keycloakx 차트는 http.relativePath 기본값을 /auth 로 두고 있어서 기존 클라이언트 설정의 /auth/realms/master 같은 주소가 그대로 살아 있습니다. 다만 Keycloak 자체는 17 부터 기본 컨텍스트 경로에서 /auth 를 뺐기 때문에, 차트 없이 직접 띄우면 –http-relative-path /auth 를 따로 줘야 예전 주소가 맞습니다.HA 구성에 헤드리스 서비스와 DNS_PING 이 아직 필요한가요?
필수는 아닙니다. Keycloak 26 의 기본 캐시 스택은 jdbc-ping 이라 같은 DB 를 쓰는 파드끼리 DB 로 서로를 찾고, keycloakx 차트도 cache.stack 기본값에서 KC_CACHE_STACK=jdbc-ping 을 넣습니다. 대신 같은 DB 를 공유하면서 따로 돌려야 하는 클러스터가 있다면 이름을 달리 주지 않으면 하나로 합쳐진다는 점은 조심하셔야 할 것 같습니다.7. 참조
https://github.com/codecentric/helm-charts/blob/master/charts/keycloak/README.md https://github.com/codecentric/helm-charts/blob/master/charts/keycloakx/README.md https://www.keycloak.org/observability/configuration-metrics https://blog.sighup.io/keycloak-ha-on-kubernetes/함께 보면 좋은 글
- [DevOps] Kubernetes HPA 실전 적용
- [DevOps] Nginx Rate Limit
- [DevOps] Nginx Log Aggregation & Dashboard
- [DevOps] Hubot 적용기
새 글이 올라오면 Threads @wonizz.ai 에도 올립니다. 팔로우해 두면 피드에서 바로 볼 수 있습니다.