[DevOps] KeyCloak 헬름차트 설치

안녕하세요? 정리하는 개발자 워니즈입니다. 이번시간에는 spring boot 에서 SSO(Single Sign On)을 해주는 KeyCloak에 대해서 정리를 해보려고 합니다. 이번 포스팅은 KeyCloak의 기능에 대해서 정리하는 것이 아닌, 설치 방법에 대해서 정리를 합니다.

1. KeyCloak 이란 무엇인가요?

keycloak은 계정관리 및 access 관리를 제공합니다. Single-sign-on이 가능한 오픈소스입니다.
application을 구축하고자 할 때 코드의 변경없이 인증과 자원보호의 기능을 제공하는 솔루션입니다. 해당 솔루션은 오픈소스로 제공되며 community 버전의 경우 별도의 비용없이 사용이 가능합니다.

개발자가 작성해야하는 보안 기능을 기본적으로 제공하며, 조직의 개별 요구 사항에 맞게 쉽게 조정가능합니다. keycloak은 로그인, 등록, 관리 및 계정관리를 위한 사용자 정의 가능한 인터페이스를 제공합니다.

2. 설치 방법

필자는 Keycloak 설치 자체를 공식 헬름 차트를 이용하여 설치했습니다. 해당 차트를 보고 필요한 부분만을 취득하여 가져왔습니다.

헬름 패키지 매니저를 사용하여 쿠버네티스 클러스터에 StatefulSet으로 설치를 진행합니다.

기본적으로 해당 차트는 PostgreSQL 차트를 의존하고 있습니다. 또한 설치시 인프라 스트럭처 안에서 PV 지원을 요구합니다.

필자는 MySQL로 진행하기 때문에 해당 부분을 변경하여 배포 하였습니다.

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이라는 공간으로 복사를 해주도록 구성했습니다.

빌드를 수행한 후, docker registry에 push를 한 후, 배포를 수행합니다.
이후에, KeyCloak Realm > Themes > Login Theme > custom으로 설정후 Save를 누릅니다.

로그인 화면이 정상적으로 바뀐것을 볼 수 있습니다.

4. 프로메테우스 메트릭 설정

KeyCloak은 2가지 방식의 메트릭을 제공합니다.

  • /metrics -> Wildfly에서 제공하는 메트릭
  • /auth/realms/master/metrics -> Keycloak에서 제공하는 메트릭

필요한 메트릭은 Realms에서 로그인하는 정보들이므로, 2번째 메트릭을 수집하는것으로 정했습니다. 이를 위해서는 별도의 jar파일의 설치가 필요합니다.

아래의 github에 접속하여 순서대로 빌드를 수행한 후, 빌드된 jar파일을 Dockerfile에서 빌드를 수행할 때 카피를 수행해줍니다.

Keycloak Metrics SPI

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를 통해서 접속하면 정상적으로 데이터를 가져오는 것을 확인 할 수 있습니다.

5. 마치며…

이번시간에는 인증/인가를 대신해주는 SSO 어플리케이션인 KeyCloak에 대해서 알아봤습니다. 다음시간에는 KeyCloak의 사용법에 대해서도 간단히 정리를 해보도록 하겠습니다.

6. 참조

https://github.com/codecentric/helm-charts/blob/master/charts/keycloak/README.md

https://blog.sighup.io/keycloak-ha-on-kubernetes/

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다