Files
keycloak-pattern/docs/guides/05-keycloak
DongHyeonkaandClaude Opus 5 88b7bd4bf0 docs(guides): hands-on guides for the seven setup stages
The experiment documents record what was found. These record what to type to
reproduce it, in folders per stage.

Two kinds of command are kept apart. 하기/확인 is what somebody actually types
at a terminal — short, one at a time. 근거를 재려면 is the long measuring form
this lab used to put evidence in a document, marked as not needed day to day.
The same split applies to curl: -I to look once, -w '%{http_code}' only when
comparing across repetitions.

No placeholders. Where a value is needed the command that produces it is
given, and secrets are checked by length rather than printed:

  TOKEN=$(ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token')
  echo "${#TOKEN} 자"

Stage 05 verifies resources in layers, because a Secret existing and a pod
having received it are different facts: keys, then length, then the value
inside the container, then which env var came from which Secret. Same for
workloads — Deployment to ReplicaSet to Pod, with the seven ReplicaSets this
cluster actually carries as the worked example.

Two commands were wrong and re-running them caught it. kubectl get endpoints
prints a deprecation warning on v1.33+, so the guide uses describe svc and
EndpointSlice. And the Keycloak image has no curl, so reading metrics from
inside the container fails with exit 127 — the guide asks Prometheus instead,
or runs a throwaway curl pod.

Read-only checks were executed against the running lab and their output is
quoted verbatim. Creating commands could not be re-run without destroying the
lab, so they are the ones used at build time; the README says which is which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 16:48:10 +09:00
..

05 — Keycloak 2노드 + PostgreSQL

이 단계가 끝나면

https://auth.hyeonworks.com 에서 관리 콘솔에 로그인되고, 두 Keycloak 이 하나의 클러스터로 보인다.

전제

04 까지 끝나 https:// 가 열린다.


1. 매니페스트를 적용한다

하기

kubectl create namespace keycloak-lab
kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml

확인 — 적용이 끝날 때까지 기다린다

kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s
partitioned roll out complete: 2 new pods have been updated...

rollout status끝날 때까지 블록한다. get pods 를 반복해서 보는 것보다 이게 낫다.


2. 리소스가 제대로 만들어졌는지 — 층별로 본다

kubectl get pods 만 보면 놓치는 것이 많다. 위에서 아래로 확인한다.

2-1. 무엇이 만들어졌나

확인

kubectl -n keycloak-lab get all

all 은 이름과 달리 전부는 아니다 — Secret·ConfigMap·PVC·Ingress 는 안 나온다.

kubectl -n keycloak-lab get secret,configmap,pvc,ingress

2-2. Deployment → ReplicaSet → Pod 사슬

Deployment 는 파드를 직접 만들지 않는다. ReplicaSet 을 만들고 그것이 파드를 만든다. 이 사슬 어디서 끊겼는지가 진단의 출발점이다.

확인

kubectl -n keycloak-lab get deploy,rs,pod -l app=bff

실측

replicaset.apps/bff-555df79c97   2   2      ← 지금 쓰이는 것
replicaset.apps/bff-574c6d658b   0   0      ← 지난 배포
replicaset.apps/bff-576d869c6d   0   0
...  (7개)
pod/bff-555df79c97-6j86w   1/1   Running

ReplicaSet 이 여러 개인 것은 정상이다. 배포할 때마다 새로 만들고 옛것은 0 으로 남긴다 — 그래서 kubectl rollout undo 가 가능하다. 파드 이름의 가운데 해시(555df79c97)가 어느 ReplicaSet 소속인지 말해 준다.

읽는 법:

보이는 것
Deployment 는 있는데 RS 가 없다 컨트롤러가 못 돌았다 — RBAC·admission 확인
RS 는 있는데 DESIRED 만 있고 CURRENT 가 0 파드를 못 만든다 — 이벤트를 본다
Pod 은 있는데 0/1 컨테이너가 안 뜬다 — 로그와 describe

StatefulSet 은 ReplicaSet 을 쓰지 않고 파드를 직접 만든다. 그래서 keycloak-0·keycloak-1 처럼 이름이 고정이고, A-4 에서 Terminating 파드가 안 지워지면 대체 파드가 안 생기는 이유가 이것이다.

2-3. Secret 이 실제로 들어갔나 — 세 층으로 본다

값이 있는 것과 파드가 그 값을 받은 것은 다르다.

확인 ① Secret 에 키가 있나 — 값은 찍지 않는다

kubectl -n keycloak-lab get secret keycloak-lab-secrets -o jsonpath='{.data}' \
  | tr ',' '\n' | grep -o '"[A-Z_]*"' | tr -d '"'
KC_BOOTSTRAP_ADMIN_PASSWORD
POSTGRES_PASSWORD

확인 ② 값이 비어 있지 않나 — 길이만

kubectl -n keycloak-lab get secret keycloak-lab-secrets \
  -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d | wc -c
22

확인 ③ 파드 안에 주입됐나 — 여기가 진짜다

kubectl -n keycloak-lab exec keycloak-0 -- \
  sh -c 'echo "길이=${#KC_BOOTSTRAP_ADMIN_PASSWORD}"'
길이=19

어느 환경변수가 어느 Secret 에서 왔는지도 볼 수 있다.

kubectl -n keycloak-lab get pod keycloak-0 \
  -o jsonpath='{range .spec.containers[0].env[*]}{.name}{"\t"}{.valueFrom.secretKeyRef.name}{"\n"}{end}'
KC_DB
KC_DB_URL
KC_DB_USERNAME
KC_DB_PASSWORD	keycloak-lab-secrets     ← Secret 에서 온 것만 오른쪽에 이름이 있다

값을 그대로 찍지 않는 습관. -o yaml 은 base64 를 그대로 보여 주고 그건 암호화가 아니다. 터미널 스크롤백·화면 공유·로그에 남는다. D-3 이 잰 것이 이것이다 — experiment-d3-secret-management.md

2-4. Service 가 파드를 잡고 있나 — Endpoints

Service 가 있어도 셀렉터가 안 맞으면 뒤가 비어 있다. 이때 증상은 「연결은 되는데 응답이 없다」라 원인을 찾기 어렵다.

확인 — 실무자가 가장 자주 쓰는 형태

kubectl -n keycloak-lab describe svc keycloak | grep -i endpoints
Endpoints:   10.42.0.67:8080,10.42.1.155:8080

목록으로 보려면 EndpointSlice 를 쓴다.

kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak
NAME             ADDRESSTYPE   PORTS   ENDPOINTS                AGE
keycloak-xdph6   IPv4          8080    10.42.0.67,10.42.1.155   3d23h

kubectl get endpoints 는 쓰지 않는다. v1.33 부터 deprecated 이고 실행하면 경고가 나온다.

Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice

옛 문서와 블로그에 이 형태가 많으니 주의한다.

준비 상태까지 함께 보려면 이렇게 뽑는다.

kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{"\t"}{.conditions.ready}{"\n"}{end}'
10.42.0.67     true
10.42.1.155    true

readyfalse 면 파드는 있는데 readiness 프로브를 통과하지 못한 것이라, Service 가 그 파드로 트래픽을 보내지 않는다.

비어 있으면 셀렉터와 파드 라벨이 안 맞는 것이다.

kubectl -n keycloak-lab get svc keycloak -o jsonpath='{.spec.selector}'; echo
kubectl -n keycloak-lab get pods --show-labels

2-5. PVC 가 실제로 붙었나

kubectl -n keycloak-lab get pvc

Pending 이면 StorageClass 가 없거나 노드에 자리가 없다. local-path 는 파드가 스케줄될 때까지 기다린다(WaitForFirstConsumer)므로, 파드가 안 뜨면 PVC 도 Pending 인 것이 정상이다. 둘이 서로를 기다리는 것처럼 보이지만 파드 쪽 원인을 먼저 본다.


3. 안 뜰 때 — 순서가 있다

① 이벤트부터. 로그보다 먼저다. 스케줄링·이미지·볼륨 실패가 여기 나온다.

kubectl -n keycloak-lab get events --sort-by=.lastTimestamp | tail -20

② describe. 그 파드에 한정된 이벤트와 상태를 함께 본다.

kubectl -n keycloak-lab describe pod keycloak-0

③ 로그. 컨테이너가 떴는데 죽는 경우다.

kubectl -n keycloak-lab logs keycloak-0
kubectl -n keycloak-lab logs keycloak-0 --previous    # 재시작 직전 로그

--previous 가 중요하다. CrashLoopBackOff 면 지금 컨테이너는 방금 뜬 것이라 죽은 이유는 이전 컨테이너 로그에 있다.

④ 그래도 모르면 안에서 본다.

kubectl -n keycloak-lab exec -it keycloak-0 -- sh

4. 클러스터가 형성됐는지 확인한다

파드가 둘 다 Running 인 것과 하나의 클러스터로 묶인 것은 다르다.

확인 ① 로그

kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
ISPN000094: Received new cluster view for channel ISPN:
  [keycloak-0-10001|1] (2) [keycloak-0-10001, keycloak-1-52537]

(2) 가 멤버 수다.

확인 ② 디스커버리 테이블

kubectl -n keycloak-lab exec deploy/postgres -- \
  psql -U keycloak -d keycloak -c 'select name, ip from jgroups_ping'

확인 ③ 지표

★ Keycloak 컨테이너에는 curl 이 없다. 공식 이미지가 최소 구성이라 wgetnc 도 없다. 안에서 치면 이렇게 된다.

sh: line 1: curl: command not found
command terminated with exit code 127

그래서 밖에서 물어본다. Prometheus 에 묻는 것이 가장 짧다.

kubectl -n observability exec deploy/prometheus -- \
  wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
keycloak-1 → 2
keycloak-0 → 2

Prometheus 가 아직 없다면 임시 파드를 띄운다.

K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
kubectl -n keycloak-lab run m --rm -i --restart=Never \
  --image=curlimages/curl:8.11.1 --quiet --command -- \
  sh -c "curl -s http://$K0:9000/metrics | grep '^vendor_cluster_size'"
vendor_cluster_size{cache_manager="keycloak",node="keycloak-0-46674"} 2.0

두 값이 다를 수 있다. 각 노드가 자기가 아는 멤버 수를 보고하므로, 분단되면 한쪽은 2 다른 쪽은 1 이 된다. 한 노드만 보면 분단을 놓친다.

셋이 다른 것을 본다. 로그는 「그때 그렇게 보였다」이고, 테이블은 「지금 등록되어 있다」이며, 지표는 「지금 그 노드가 그렇게 안다」이다. A-1 에서 이 셋이 갈렸다 — 테이블에는 둘 다 있는데 메시지는 안 갔다.


5. 밖에서 닿는지

확인

curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
200

브라우저로 https://auth.hyeonworks.com/admin 에 들어가 관리자로 로그인한다. 비밀번호는 위 2-3 의 Secret 에 있다.


막히면

증상 어디를 보나
파드가 Pending describe pod 의 Events — 스케줄 불가 사유
ImagePullBackOff 이미지 이름·태그. 자체 빌드면 두 노드 모두에 반입했는가
CrashLoopBackOff logs --previous
Running 인데 0/1 readiness 프로브 실패. describe 의 Conditions
밖에서 502 Ingress → Service → Endpoints 순으로 뒤를 본다
클러스터가 1로 보임 7800 이 막혔거나 디스커버리 실패. 위 4번 셋 다 확인

근거를 재려면 (선택)

세션이 실제로 어디 저장되는지는 DB 를 직접 본다.

kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
  -c "select offline_flag, count(*) from offline_user_session group by 1"

offline_flag='0' 이 온라인 세션이다. 로그인하고 이 수가 늘면 persistent-user-sessions 가 켜져 있는 것이고, 안 늘면 메모리에만 있는 것이다. 그 차이가 A층 결론 전체를 뒤집는다 — A-7