Files
keycloak-pattern/docs/guides/05-keycloak/README.md
T
DongHyeonkaandClaude Opus 5 8062cc9a19 docs(guides): bring the seven setup guides in line with the practitioner skill
The guides were written before the skill existed and a scored audit found
three gaps. Fixed by a subagent running under the skill, with measured
values, versions, IPs and quoted output declared off limits.

The big one: every step showed a command and its output, and almost none
said which line to look at or what it meant. 64 interpretation pairs added
across the seven files, weighted where the reading is hardest — 20 in the
Keycloak stage, where a Secret existing and a pod having received it are
different facts.

Only the extracting form of curl appeared. Where the reader meets a response
for the first time the guides now open with curl -I or curl -v and name the
lines worth reading; -w '%{http_code}' survives only where the code is a
value being compared — two upstream nodes against each other, or the 900-run
control loop.

Listing Secret keys went from a three-stage pipe to kubectl describe secret,
which prints the key names and their byte counts in one native command
without exposing a value.

And a tool assumption: jq and yamllint are installed on neither the lab host
nor the guests. The guides now say so where JSON is read by eye, rather than
sending the reader to install something mid-diagnosis. cloud-init schema is
on the guests and is now the guest-side check.

Also removes a stray Playwright screenshot committed at the repository root
in 919547a; the evidence copy under docs/evidence/b7a-orphan-session/ is the
one the document references.

Four things the audit left standing are recorded in the agent's report rather
than papered over — notably that 04's reload measurements are stated without
a reproduction procedure, and that 05 and 06 reference each other as
prerequisites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 17:33:31 +09:00

23 KiB

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...

어디를 봐야 하는가 — 이 명령은 끝날 때까지 아무것도 안 찍고 멈춰 있다. 그 침묵이 정상이다. 마지막에 나오는 한 줄에서 complete 라는 낱말과 파드 개수 2 를 본다. 300초를 다 쓰고 타임아웃으로 끝나면 그것도 답이다 — 「안 떴다」가 확정된 것이니 3번으로 간다.

이 결과가 의미하는 것complete 면 두 파드가 다 Ready 가 됐다는 뜻이라 2번의 층별 확인으로 넘어간다. rollout status 를 쓰는 이유는 get pods 를 반복해서 치는 것보다 나아서만이 아니라, 언제 끝났는지를 사람이 판정하지 않아도 되기 때문이다. StatefulSet 은 파드를 하나씩 순서대로 띄우므로 중간에 0/2 로 한참 멈춰 있는 것은 정상이다.


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

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

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

확인 — 이 네임스페이스에 무엇이 서 있는가

kubectl -n keycloak-lab get all

어디를 봐야 하는가 — 종류별로 묶여 나오는 왼쪽 이름 열을 훑고, 파드 줄에서는 READY 칸의 1/1RESTARTS 칸을 본다. RESTARTS 가 0 이 아니면 지금은 Running 이어도 한 번 죽었다 살아난 것이라, 3번의 logs --previous 를 볼 이유가 된다.

이 결과가 의미하는 것 — 여기 보이는 것이 워크로드의 전부다. 그런데 all 은 이름과 달리 전부는 아니다 — Secret·ConfigMap·PVC·Ingress 는 안 나온다. 이 넷이 빠졌다는 사실을 모르고 「다 만들어졌다」고 판정하는 것이 흔한 오독이라, 한 번 더 친다.

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

어디를 봐야 하는가 — 네 종류가 하나씩이라도 있는가. PVC 줄의 STATUS 는 Bound 인가, Ingress 줄의 HOSTS 칸이 auth.hyeonworks.com 인가.

이 결과가 의미하는 것 — 매니페스트에 있는데 여기 없는 종류가 있다면 apply 가 부분적으로만 먹은 것이다. Ingress 의 호스트 이름이 04 에서 발급한 인증서의 이름과 다르면, 밖에서는 TLS 는 되는데 404 가 나온다.

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 줄들 중 DESIRED 가 0 이 아닌 것 하나 를 찾고, 그 이름의 해시를 파드 이름 가운데 해시와 맞춰 본다. 위 실측에서는 555df79c97 이 그 짝이다. 나머지 0 0 줄들은 읽지 않아도 된다.

이 결과가 의미하는 것ReplicaSet 이 여러 개인 것은 정상이다. 배포할 때마다 새로 만들고 옛것은 0 으로 남긴다 — 그래서 kubectl rollout undo 가 가능하다. 파드가 옛 해시를 달고 있으면 새 배포가 아직 안 넘어온 것이고, 그 상태로 실험하면 고친 적 없는 코드를 재게 된다. 사슬이 어디서 끊겼는지는 이렇게 읽는다.

보이는 것
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 describe secret keycloak-lab-secrets

실측 — 아래쪽 Data 절만 옮긴 것이다

Data
====
KC_BOOTSTRAP_ADMIN_PASSWORD:  19 bytes
POSTGRES_PASSWORD:            22 bytes

어디를 봐야 하는가Data 절의 키 이름과 그 옆의 바이트 수 두 칸. describe 는 값을 절대 찍지 않고 길이만 보여 준다 — 그래서 키 목록 확인과 「비어 있지 않은가」 확인이 한 명령으로 끝난다. 0 bytes 인 키가 있으면 그 자리가 비어 있는 것이다.

이 결과가 의미하는 것 — 매니페스트가 기대하는 키 이름이 여기 그대로 있어야 한다. 이름이 하나라도 다르면 파드는 CreateContainerConfigError 로 멈추고, 이유는 describe pod 의 Events 에 키 이름까지 적혀 나온다. 바이트 수가 뜻밖에 크면(예: 20 이어야 할 것이 21) echo 로 만들면서 개행이 같이 들어간 경우다 — 흔한 사고이고, 증상은 「비밀번호가 틀렸다」로 나온다.

-o yaml 로 보지 않는다. base64 는 암호화가 아니라 인코딩이라 화면·스크롤백·화면 공유·터미널 로그에 값이 그대로 남는다. D-3 이 잰 것이 이것이다 — experiment-d3-secret-management.md

확인 ② 특정 키 하나를 따져 볼 때 — 길이만

describe 가 보여 주는 바이트 수는 base64 를 푼 뒤의 길이다. 어떤 키 하나가 의심스러워 다시 잴 때만 이 형태를 쓴다.

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

어디를 봐야 하는가 — 숫자 하나. 그리고 그것이 확인 ①의 22 bytes 와 같은가.

이 결과가 의미하는 것 — 두 값이 같으면 Secret 쪽은 더 볼 것이 없다. base64: invalid input 이 나오면 키 이름을 잘못 쓴 것이다(없는 키는 빈 문자열로 나온다). 여기까지는 Secret 안에 무엇이 있나이고, 파드가 그것을 받았는지는 아직 모른다.

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

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

어디를 봐야 하는가 — 숫자 하나. ${#VAR} 는 값이 아니라 글자 수만 내놓는다. 이것이 확인 ①의 19 bytes 와 같은가.

이 결과가 의미하는 것 — 같으면 Secret → 파드 환경변수까지 이어졌다. 길이=0 이면 Secret 에는 있는데 이 파드가 그것을 안 받은 것이다 — envFrom/valueFrom 을 빠뜨렸거나, 파드가 Secret 을 고치기 전에 떠서 옛 값을 들고 있는 경우다(환경변수로 주입한 Secret 은 값을 바꿔도 파드를 다시 만들기 전까지 갱신되지 않는다).

어느 환경변수가 어느 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 에서 온 것만 오른쪽에 이름이 있다

어디를 봐야 하는가오른쪽 칸이 채워진 줄만. 왼쪽만 있는 줄은 매니페스트에 값이 그대로 적힌 것이고, 오른쪽에 이름이 있는 줄이 Secret 을 참조하는 것이다.

이 결과가 의미하는 것 — 비밀이어야 할 변수의 오른쪽이 비어 있으면 그 값은 매니페스트에 평문으로 적혀 있다는 뜻이고, 그 파일은 대개 git 에 들어간다. 여기서는 KC_DB_PASSWORD 만 Secret 에서 온다.

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

어디를 봐야 하는가 — 쉼표로 갈린 주소가 몇 개인가, 그리고 그 IP 들이 kubectl -n keycloak-lab get pods -o wide 의 파드 IP 와 같은가. 포트 번호가 컨테이너가 실제로 듣는 포트인가도 함께 본다.

이 결과가 의미하는 것 — 두 개면 Service 가 두 파드를 다 잡고 있다. 비어 있으면 Service 는 있는데 뒤가 없는 것이고, 이때 증상은 「연결은 되는데 응답이 없다」라 원인이 Service 에 있다는 것이 잘 안 보인다. 하나뿐이면 나머지 한 파드가 readiness 를 통과하지 못한 것이라, 그 상태로 A층 실험을 하면 이미 한쪽으로만 가고 있던 트래픽을 이중화 실패로 오독하게 된다.

목록으로 보려면 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

어디를 봐야 하는가 — 오른쪽 칸이 두 줄 다 true 인가. 여기서만 값을 뽑는 형태를 쓰는 이유는, 이 두 칸이 A층 실험 전후로 반복해서 비교할 값이기 때문이다. 처음 볼 때는 위의 describe svc 로 충분하다.

이 결과가 의미하는 것readyfalse 면 파드는 있는데 readiness 프로브를 통과하지 못한 것이라, Service 가 그 파드로 트래픽을 보내지 않는다. 파드 목록에서는 Running 으로 보이므로 get pods 만 봐서는 알 수 없다 — 0/1 인지 1/1 인지가 같은 사실을 말해 준다.

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

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

어디를 봐야 하는가 — STATUS 칸(Bound/Pending)과 VOLUME 칸(비어 있는지), 그리고 STORAGECLASS 칸이 local-path 인가. StatefulSet 이면 PVC 이름 끝에 파드 번호가 붙어 있어(...-keycloak-0) 어느 파드 것인지 바로 보인다.

이 결과가 의미하는 것Bound 면 볼륨이 붙었다. Pending 이면 StorageClass 가 없거나 노드에 자리가 없다. local-path 는 파드가 스케줄될 때까지 기다린다(WaitForFirstConsumer)므로, 파드가 안 뜨면 PVC 도 Pending 인 것이 정상이다. 둘이 서로를 기다리는 것처럼 보이지만 파드 쪽 원인을 먼저 본다. 사유는 PVC 의 이벤트에 적혀 있다.

kubectl -n keycloak-lab describe pvc      # 이름을 안 주면 전부 나온다

각 PVC 절의 맨 아래 Eventswaiting for first consumer 인지 no persistent volumes available 인지가 적혀 있고, 둘은 대응이 다르다 — 앞엣것은 파드를 고치는 문제, 뒤엣것은 저장소를 고치는 문제다.


3. 안 뜰 때 — 순서가 있다

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

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

어디를 봐야 하는가 — 맨 아랫줄부터 거꾸로 읽는다. --sort-by 를 준 이유가 그것이다. TYPE 이 Warning 인 줄, REASON 칸(FailedScheduling·Failed· BackOff), 그리고 OBJECT 칸이 어느 파드인가. 이벤트는 기본 한 시간만 남는다 — 아무것도 없으면 「문제가 없다」가 아니라 「이미 지워졌다」일 수 있다.

이 결과가 의미하는 것 — REASON 하나가 다음 행동을 정한다. FailedScheduling 은 노드에 자리가 없는 것이라 파드가 아니라 클러스터를 봐야 하고, ErrImagePull 은 이미지 이름 문제라 로그를 볼 것도 없다. BackOff 는 컨테이너가 떴다가 죽는 중이라는 뜻이라 ③으로 간다.

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

kubectl -n keycloak-lab describe pod keycloak-0

어디를 봐야 하는가 — 위에서부터 세 곳이다. Conditions 절에서 ReadyFalse 인가, 컨테이너 절의 State/Last State 와 그 안의 Exit Code, 그리고 맨 아래 Events. Exit Code 는 그것만으로 말이 된다 — 137 은 OOM 이나 강제 종료, 1 은 애플리케이션이 스스로 끝낸 것, 127 은 명령을 못 찾은 것이다.

이 결과가 의미하는 것 — Exit Code 가 137 이면 로그에는 아무 단서도 없을 수 있다(맞아 죽은 쪽은 유언을 못 남긴다) — 메모리 한도를 본다. ReadyFalse 이고 컨테이너는 살아 있으면 readiness 프로브 문제이므로 2-4 로 돌아간다.

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

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

어디를 봐야 하는가 — 첫 명령에서는 마지막 줄들, 둘째 명령에서는 스택 트레이스의 맨 윗줄(가장 안쪽 예외가 아니라 최초 원인 줄)이다. Keycloak 은 기동에 성공하면 Keycloak ... started in 한 줄을 남기므로, 그 줄이 있는지 없는지가 「기동 중」과 「기동 실패」를 가른다.

이 결과가 의미하는 것--previous 가 중요하다. CrashLoopBackOff 면 지금 컨테이너는 방금 뜬 것이라 죽은 이유는 이전 컨테이너 로그에 있다. --previousnot found 를 내면 아직 한 번도 재시작하지 않은 것이고, 그러면 지금 로그가 곧 전부다.

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

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) 가 멤버 수, 대괄호 안의 이름 목록, 그리고 |1뷰 번호(멤버가 들고 날 때마다 올라간다). tail -1 을 붙였으므로 지금 보고 있는 것은 가장 최근 뷰 하나다.

이 결과가 의미하는 것(2) 면 이 노드는 상대를 봤다. (1) 이면 혼자 있다고 알고 있는 것이다. 뷰 번호가 계속 오르고 있으면 멤버가 붙었다 떨어지기를 반복하는 중이라, 「지금 2 다」라는 스냅숏보다 그 사실이 더 중요하다. 이 줄은 「그때 그렇게 보였다」는 과거형이다 — 지금 상태는 확인 ③에서 본다. grep 이 아무것도 못 찾으면 클러스터링을 아직 시작도 못 한 것이니 로그를 통째로 본다.

확인 ② 디스커버리 테이블

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

어디를 봐야 하는가 — 나온 행이 몇 개인가, 그리고 ip 칸이 2-4 에서 본 파드 IP 와 같은가. 끝의 (N rows) 한 줄이 개수를 말해 준다.

이 결과가 의미하는 것 — 이 표는 「등록되어 있다」이지 「서로 말이 통한다」가 아니다. 두 행이 다 있는데 확인 ①이 (1) 이면, 서로를 찾기는 했는데 7800 포트로 메시지가 안 가는 것이다 — A-1 에서 정확히 그 일이 벌어졌다. 옛 파드의 행이 남아 있을 수도 있으므로 IP 를 지금 파드와 대조한다.

확인 ③ 지표

★ 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'

어디를 봐야 하는가 — 응답은 줄바꿈 없는 JSON 한 줄로 온다. data.result 배열에서 원소가 몇 개인가(노드 수), 각 원소에서 두 군데만 읽는다 — "metric" 안의 node 라벨(어느 Keycloak 인가)과 "value" 배열의 둘째 원소(따옴표에 싸인 값). 이 실험대에는 jq 가 없다. 파서를 따로 짜지 말고 화면에 나온 JSON 을 그대로 읽는다.

실측 — 그렇게 읽어낸 값이다

keycloak-1 → 2
keycloak-0 → 2

이 결과가 의미하는 것 — 원소가 둘이고 값이 둘 다 2 면 두 노드가 서로를 보고 있다. 원소가 하나뿐이면 나머지 노드의 스크레이프가 실패한 것이라 클러스터 문제가 아니라 관측 문제일 수 있다 — 06 의 targets 를 본다.

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

어디를 봐야 하는가 — 줄 끝의 숫자와, 중괄호 안 node= 라벨이 어느 파드인가. 이 형태는 그 파드 하나에게 직접 물은 것이라 라벨의 노드 이름과 $K0 로 고른 파드가 반드시 일치한다.

이 결과가 의미하는 것--rm 을 붙였으므로 파드는 끝나면 사라진다. curlimages/curl 을 쓰는 이유는 Keycloak 이미지에 도구가 없기 때문이고, 같은 이유로 이 방법은 Keycloak 뿐 아니라 최소 이미지 전부에 쓴다. 값이 안 나오고 연결 거부가 나면 9000(관리 포트)이 안 열린 것이다.

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

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


5. 밖에서 닿는지

확인 — 2홉을 다 지나 파드까지 닿는가

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

어디를 봐야 하는가 — 코드 한 칸. 여기서 값만 뽑는 형태를 쓰는 것은 03·04 에서 잰 것과 같은 명령으로 같은 값이 나오는지 비교하는 것이 목적이기 때문이다. 이 자리에서 처음 보는 것이 아니다.

이 결과가 의미하는 것200 이면 nginx → Traefik → Ingress → Service → 파드가 전부 이어졌다. 502·503 이면 뒤에서부터 되짚는다 — Ingress 가 있는지(2-1), Service 뒤에 파드가 있는지(2-4), 파드가 Ready 인지(2-2) 순서다. 처음 보는 오류라 헤더가 필요하면 값만 뽑는 형태를 버리고 읽는 형태로 바꾼다.

curl -I https://auth.hyeonworks.com/realms/master

브라우저로 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_flag0 인 행의 count. 로그인 전과 후에 두 번 재서 그 수의 차이를 본다. 한 번만 재면 아무것도 알 수 없다. 행이 아예 없으면(0 rows) 표는 있는데 비어 있는 것이다.

이 결과가 의미하는 것 — 로그인 뒤 수가 늘면 세션이 DB 에 남는 것 (persistent-user-sessions 켜짐)이고, 안 늘면 메모리에만 있는 것이다. 그 차이가 A층 결론 전체를 뒤집는다 — 메모리에만 있으면 파드를 재시작하는 순간 세션이 사라지고, DB 에 있으면 살아남는다. A-7