Files
document-haness/docs/virtualization/tech-log-studio/lab-environment-build/setup/setup-keycloak-two-nodes-and-postgres-on-k3s.md
T
DongHyeonkaandClaude Opus 5 32e39e20aa fix(setup): 실험대에서 35편을 끝까지 밟고 어긋난 명령·결과 31건을 고친다
test-server 를 비우고 다시 세운 뒤 Setup 기록 35편(virtualization 9 ·
keycloak-session-store 26)을 문서에 적힌 명령 그대로 쳤다. 어긋난 자리를
기록과 SSOT 양쪽에 실측과 함께 넣었다.

막히던 것
- 04 의 인증서 경로가 live/hyeonworks.com 이라 nginx 가 [emerg] 로 안 떴다.
  실제 계보는 live/auth.hyeonworks.com 이고 「문제가 생기면」은 진단이 거꾸로였다
- 인증서가 와일드카드가 아니다. SAN 이 auth·app1·app2 셋뿐이라 그 밖의 이름은
  TLS 에서 끊기고 curl 이 exit 60 · %{http_code} 000 을 낸다. SSOT 안에서
  두 문단이 서로 어긋나 있었다
- A-7 14번 ①이 kc-lab-1 에서 여섯 줄 다 실패하는데 마지막 date 만 「차단」을 찍는다

검사가 실패할 수 없던 자리
- B-1 의 세션 키 고르기는 앞 단계가 $KEY 를 채워 둬서 루프가 한 건도 못 맞혀도
  통과한다. KEY= 로 비우고 키마다 1/0 을 찍게 바꿨다
- k3s-agent 유닛의 sed -i 는 패턴에 $HOME 이 들어 있어 아무 줄도 안 바꾼 채 성공한다

certbot
- renew --dry-run 의 종료 코드는 성공도 0, 실패도 0, 다른 사유의 실패는 1 이다.
  본문의 renew failure(s) 로만 판정할 수 있다
- --dry-run 은 staging 서버를 쓰는데 renewal/*.conf 의 account= 는 운영 계정을
  가리킨다. 실패한 dry-run 이 staging 계정을 하나 더 만들어 다음 실행이 계속 멎는다
- 훅을 755 로 놓고 시뮬레이션이 성공해도 Running deploy-hook command 는 안 나온다.
  certbot 2.1.0 에는 --run-deploy-hooks 도 없다
- 강제 갱신은 실제로 쳤고 서빙까지 닿았다. serial 06F3E0EF…1373 → 065547…3DF1,
  notAfter Dec 3 → Dec 16, nginx worker 2629 4712 → 4745 4754

독자가 칠 수 있는 형태로
- 안 되는 형태가 번호 붙은 단계에 앉아 있던 8곳을 뒤집고, 되는 형태를 ①로 올렸다
- 랩 안에서 공개 이름을 치는 curl 65줄에 --resolve 를 붙였다. 붙인 형태를 실제로
  쳐서 문서가 적은 값과 같은지 확인했다
- 힙독·sed -i·echo >>·&&·|| 를 편집기 + 파일 리스팅 + 분할 형태로 바꿨다
- 닫는 코드펜스가 빠져 뒤 200여 줄의 블록 종류가 뒤집혀 있던 곳을 포함해 3곳을 고쳤다

관문: check_body PASS · check_prose error 0 · check_evidence 두 프로젝트 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0 · 코드펜스 전수 0건

남은 것: B-0 주입은 keycloak-pattern 저장소의 소스를 고치고 이미지를 다시 구워야
해서 안 했다(unknown).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:38:12 +09:00

38 KiB

id, kind, slug, title, topic, topicName, project, status, studio, pinnedVersions, sourceRevision, source
id kind slug title topic topicName project status studio pinnedVersions sourceRevision source
7b113a04-180a-40ec-9270-033531c22221 SETUP keycloak-two-nodes-and-postgres-on-k3s Keycloak 2노드와 PostgreSQL 을 k3s 에 올리고 클러스터가 묶였는지 확인한다 lab-environment-build 실험대 환경 구성 virtualization 게시 전 https://hyeonworks.com/studio/documents/7b113a04-180a-40ec-9270-033531c22221/edit
name version
k3s v1.36.4+k3s1
name version
curlimages/curl 8.11.1
9465582b5d1630eb4ae7c4e078021486919bf6b6
final/document.md#191-단계-05-keycloak-2노드와-postgresql
final/document.md#184-이-부의-출처와-범위

Keycloak 2노드와 PostgreSQL 을 k3s 에 올리고 클러스터가 묶였는지 확인한다

매니페스트 한 장을 apply 해 네임스페이스 keycloak-lab 에 Keycloak 두 대와 PostgreSQL 하나를 세우는 절차다. 세우는 명령은 두 줄이고, 나머지는 그 둘이 하나의 클러스터로 묶였는지 확인하는 명령이다.

관계

  • k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다 여기 적힌 kubectl 이 도는 클러스터를 그 단계가 세운다. INTERNAL-IP 가 어긋난 채로 왔으면 여기서 Endpoints 가 한 줄만 나온다.
  • Prometheus 와 Grafana 를 올려 클러스터 안을 밖에서 본다 클러스터 크기를 지표로 묻는 확인 명령이 그 스택을 전제한다. 아직 없으면 임시 파드를 띄워 같은 값을 받는다.
  • DNS-01 으로 와일드카드 인증서를 받고 갱신이 서빙까지 닿게 한다 밖에서 도메인으로 200 을 받는 마지막 확인이 그 단계가 올린 443 을 지난다.
  • 도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다 kubectl get all 이 이름과 달리 전부를 세지 않고, 로그와 테이블과 지표가 각각 다른 시점을 말한다. 그 구분이 이 단계의 판정을 셋으로 가른다.
  • 가까운 층부터 한 층씩 건너뛰며 확인한다 — 층마다 성공 신호가 다르다 밖에서 502 가 나올 때 Ingress 에서 Service 로, Service 에서 Endpoints 로 되짚는 순서가 그 기준에서 나온다.

본문

읽기 전에 — 어디서 치는가

이 단계는 앞의 셋보다 단순하다. 전부 [lab host] 에서 치고, 저장소 루트에서 친다. 마지막 로그인 확인만 브라우저다.

번호 무엇 어디서
1 매니페스트 적용 [lab host]~/workspace/keycloak-pattern 안에서
2 파드 둘이 설 때까지 기다리기 [lab host]
판정 리소스 · 클러스터 · 밖에서 [lab host]
판정 관리 콘솔 로그인 브라우저

kubectl 이 lab host 에서 도는 까닭은 앞 단계에 있다. kubeconfig 를 게스트에서 호스트로 가져다 두었으므로 게스트에 들어갈 일이 없다. deploy/... 로 시작하는 상대경로는 저장소 루트 기준이라, 다른 디렉터리에서 치면 error: the path ... does not exist 로 막힌다.

이 단계에는 손으로 쓰는 파일이 없다. 세우는 것이 전부 매니페스트 한 장에 들어 있고, 그 원문은 저장소의 deploy/lab/k8s/keycloak-cluster.yaml 이다.

공개 이름은 랩 안에서 안 풀린다. auth.hyeonworks.com 같은 공개 이름이 랩 호스트에서도 게스트에서도 호스트 자신의 tailnet 주소 100.83.212.4 로 풀리는데 그 주소에는 443 을 듣는 것이 없다. 이름만 치면 curl000 을 낸다(2026-09-17, 랩 호스트와 kc-lab-1 양쪽에서 쳐서 확인했다, observed). 그래서 랩 안에서 치는 curl 에는 --resolve <이름>:443:192.168.122.10 을 붙여 엣지 게스트를 짚었고, 그 형태로는 정문이 200 이다(observed). tailnet 에 붙은 다른 기계에서 치면 이름 그대로 닿으므로 --resolve 가 필요 없다.

이 단계가 세우는 것

가이드 05 의 「이 단계가 끝나면」은 두 줄이다.

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

두 마디가 따로다. 「로그인된다」는 밖에서 잰 200 과 브라우저이고, 「하나의 클러스터로 보인다」는 아래 세 확인이다. 파드가 둘 다 Running 인 것과 하나의 클러스터로 묶인 것은 다르다.

무엇
네임스페이스 keycloak-lab — 매니페스트 첫 문서가 kind: Namespaceapply 가 같이 만든다
Keycloak StatefulSet keycloak — 파드 keycloak-0 · keycloak-1
PostgreSQL Deployment postgres — ReplicaSet postgres-7b474b88c8
Service keycloak → Endpoints 10.42.0.67:8080,10.42.1.155:8080
Secret keycloak-lab-secretsKC_BOOTSTRAP_ADMIN_PASSWORD 19 bytes · POSTGRES_PASSWORD 22 bytes
PVC postgres-data 하나. StorageClass local-path파드 번호가 붙지 않는다(판정 ⑤)
Ingress HOSTS 가 auth.hyeonworks.com
클러스터링 JGroups — 디스커버리 테이블 jgroups_ping, 메시지 포트 7800
관리 포트 9000/metrics 가 거기 있다

이미지 태그는 매니페스트가 갖는다. 가이드 05 는 그것을 적지 않았지만 2026-09-17 에 keycloak-pattern @ 9465582bdeploy/lab/k8s/keycloak-cluster.yaml 을 열어 읽었다(observed).

image: postgres:16-alpine
image: quay.io/keycloak/keycloak:26.7.0

파드 자원 한도와 프로브 설정과 persistent-user-sessions 설정값은 아직 이 기록으로 옮기지 않았다(unknown).

전제와 되돌리기

전제는 한 줄이다 — 앞 단계까지 끝나 https 가 열린다.

되돌리는 절차는 원본 가이드 05 에 없다(unknown). kubectl delete -f 도, 지우는 순서도 적혀 있지 않다. 이 단계가 클러스터에 남기는 것은 keycloak-lab 네임스페이스와 그 안의 전부이고, 그중 PVC 는 성격이 다르다 — StorageClass 가 local-path 라 데이터가 파드가 스케줄된 그 노드의 디스크에 놓인다. 네임스페이스를 지울 때 그 디렉터리가 어떻게 되는지는 가이드가 적지 않았고 이 실험대도 확인하지 않았다.

세우기 전에 먼저 본다

두 확인은 아무것도 바꾸지 않는다. 하나는 명령이 파일을 찾을 수 있는 곳인지 보고, 하나는 이 이름이 아직 Keycloak 이 아님을 확인한다.

확인 ① 지금 이 디렉터리에서 매니페스트가 보이는가

cd ~/workspace/keycloak-pattern
ls deploy/lab/k8s/

어디를 봐야 하는가keycloak-cluster.yamlobservability.yaml 이 보이는가.

이 결과가 의미하는 것 — 보이면 아래 kubectl apply -f deploy/... 가 파일을 찾는다. 안 보이면 첫 단계의 저장소 받기로 돌아간다 — 한 번 세웠다 철거한 뒤에는 VM 과 디스크만 지워지고 저장소는 각자 관리라 이 디렉터리가 없는 상태가 흔하다.

확인 ② 이 이름이 아직 Keycloak 이 아니다

curl -s -o /dev/null -w '%{http_code} tls=%{ssl_verify_result}\n' --resolve auth.hyeonworks.com:443:192.168.122.10 \
  https://auth.hyeonworks.com/

어디를 봐야 하는가 — 앞 단계에서 잰 404 tls=0 이 그대로인가. 앞의 코드를 같이 본다.

tls=0 만 보면 안 된다. 연결이 아예 안 됐을 때도 tls=0 이 나온다 — %{ssl_verify_result} 는 TLS 검증까지 갔을 때만 뜻이 있고 못 갔으면 초기값 0 이 그대로 찍힌다. 2026-09-17 에 앞 단계를 건너뛴 상태에서 재 보니 이렇게 나왔다(observed).

000 tls=0

000 은 응답이 없었다는 뜻이다. 판정은 코드가 404 인 것과 tls=0 이 같이 나오는 것으로 한다.

이 결과가 의미하는 것 — 가이드가 그 값 옆에 한 줄을 적어 두었다 — 앞의 404 는 이 단계 이후에 200 으로 바뀐다. 지금 재 두면 아래의 200 이 이 단계가 만든 변화인지가 분명해진다.

실행 절차

1. 매니페스트 한 장을 적용한다

목적 — 네임스페이스부터 Ingress 까지 한 파일로 세운다.

cd ~/workspace/keycloak-pattern
kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml

예상 결과 — 만든 객체가 줄마다 찍힌다. 파드가 서는 것은 그다음이라 이 출력만으로 끝났다고 판정하지 않는다.

왜 필요한가 — 네임스페이스를 따로 만들지 않는다. 매니페스트 첫 문서가 kind: Namespaceapply 가 같이 만들고, kubectl create namespace 를 먼저 치면 두 번째 실행부터 AlreadyExists 로 막힌다.

문제가 생기면 — 적용 자체가 거절되면 클러스터가 서 있는지부터 본다. 앞 단계의 kubectl get nodes 가 두 줄을 내는지가 전제다. error: the path ... does not exist 면 저장소 루트가 아닌 곳에서 쳤다.

2. 두 파드가 다 설 때까지 기다린다

목적 — Keycloak StatefulSet 의 파드 둘이 Ready 가 될 때까지 다음 명령을 치지 않는다.

kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s

예상 결과 — 2026-09-17 에 갓 세운 클러스터에서 잰 것이다(observed).

Waiting for 2 pods to be ready...
Waiting for 1 pods to be ready...
partitioned roll out complete: 2 new pods have been updated...

마지막 한 줄에서 complete 라는 낱말과 파드 개수 2 를 본다. 그 줄이 나올 때까지 명령이 안 끝나고 멈춰 있는 것이 정상이다. 다만 아무것도 안 찍고 멈춰 있지는 않다Waiting for … 줄은 진행 상황이지 오류가 아니다. 숫자가 줄어들수록 파드가 하나씩 Ready 가 된다. StatefulSet 은 파드를 하나씩 순서대로 띄우므로 중간에 0/2 로 한참 멈춰 있는 것도 정상이다.

왜 필요한가 — 300초를 다 쓰고 타임아웃으로 끝나면 그것도 답이 된다. 「안 떴다」가 확정되고 진단으로 넘어간다. rollout status 를 쓰는 까닭은 get pods 를 반복해서 치는 것보다 나아서만이 아니라, 언제 끝났는지를 사람이 판정하지 않아도 되기 때문이다. 기다리지 않고 판정으로 가면 아직 안 뜬 것과 못 뜨는 것이 섞인다.

문제가 생기면 — 타임아웃으로 끝났으면 아래 「막히면」의 순서대로 본다.

구성 값

만들어지는 객체는 「이 단계가 세우는 것」의 표에 있고, 여기서는 그 값들이 어디서 왔는지를 적는다.

어디서 정해지나
네임스페이스 이름 매니페스트 첫 문서
파드 이름의 순번 StatefulSet 이라 keycloak-0 · keycloak-1
파드 이름의 해시 Deployment 가 만든 ReplicaSet 이름에서 물려받는다
Secret 의 바이트 수 매니페스트가 넣은 값의 길이. 이 문서는 값을 적지 않는다
PVC 가 붙는 노드 local-path 가 파드 스케줄을 기다렸다 그 노드에 만든다
Ingress 의 HOSTS 앞 단계에서 발급한 인증서의 이름과 같아야 한다

Ingress 의 호스트 이름이 인증서의 이름과 다르면, 밖에서는 TLS 는 되는데 404 가 나온다.

끝났는지 판정한다

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

확인 ① 무엇이 만들어졌나

무엇을 확인하는가 — 이 네임스페이스에 무엇이 서 있는지.

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

어디를 봐야 하는가 — ① 은 종류별로 묶여 나오는 왼쪽 이름 열을 훑고, 파드 줄에서는 READY 칸의 1/1 과 RESTARTS 칸을 본다. RESTARTS 가 0 이 아니면 지금은 Running 이어도 한 번 죽었다 살아난 것이다. ② 는 네 종류가 하나씩이라도 있는가, PVC 줄의 STATUS 가 Bound 인가, Ingress 줄의 HOSTS 칸이 auth.hyeonworks.com 인가.

이 결과가 의미하는 것get all 은 이름과 달리 전부가 아니다. Secret 과 ConfigMap 과 PVC 와 Ingress 가 ① 의 출력에 나오지 않으므로, 첫 명령만 보고 「다 만들어졌다」로 판정하면 빠진 것을 모른 채 다음으로 간다. 매니페스트에 있는데 ② 에 없는 종류가 있다면 apply 가 부분적으로만 먹었다.

확인 ② Deployment 에서 ReplicaSet 을 지나 파드까지 이어졌나

무엇을 확인하는가 — 사슬 어디까지 갔는지. Deployment 는 파드를 직접 만들지 않고 ReplicaSet 을 만들며 그것이 파드를 만든다.

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

실측(observed) — 2026-09-11

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/postgres-7b474b88c8   1         1         1       80m

NAME                            READY   STATUS    RESTARTS   AGE
pod/postgres-7b474b88c8-ll87p   1/1     Running   0          80m

어디를 봐야 하는가 — ReplicaSet 이름의 해시가 파드 이름 가운데 해시와 같은가, 그리고 DESIREDCURRENTREADY 세 숫자가 다 1 인가.

이 결과가 의미하는 것 — 이 출력에 Deployment 줄이 없는 것도 정상이다. 이 매니페스트는 app: postgres 라벨을 파드 템플릿에만 달았고, ReplicaSet 과 파드는 템플릿에서 라벨을 물려받지만 Deployment 는 물려받지 않는다. Deployment 를 보려면 라벨 없이 친다.

kubectl -n keycloak-lab get deploy

StatefulSet 쪽은 사슬이 한 마디 짧다. ReplicaSet 을 만들지 않고 파드를 직접 만들어서 keycloak-0 처럼 순번 이름이 붙는데, 해시를 끼워 넣을 중간 객체가 없기 때문이다. 이름이 고정이라 같은 이름의 파드가 둘일 수 없고, 그래서 Terminating 파드가 안 지워지면 대체 파드도 안 생긴다. 가이드는 그 성질이 어느 실험에서 어떻게 나타나는지까지 적어 두었는데 그 실험 문서는 여기 반입되지 않았고, 이 실험대가 그 상태를 재현해 본 적도 없다.

kubectl -n keycloak-lab get sts,rs,pod -l app=keycloak
NAME             READY   STATUS    RESTARTS   AGE
pod/keycloak-0   1/1     Running   0          19m
pod/keycloak-1   1/1     Running   0          19m

배포를 여러 번 한 Deployment 는 ReplicaSet 이 여러 개 쌓인다. 새로 만들고 옛것은 0 으로 남기기 때문이고, 그래서 kubectl rollout undo 가 가능하다. 파드가 옛 해시를 달고 있으면 새 배포가 아직 안 넘어온 것이고, 그 상태로 실험하면 고친 적 없는 코드를 재게 된다.

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

확인 ③ Secret 이 파드까지 이어졌나

무엇을 확인하는가 — 값이 있는 것과 파드가 그 값을 받은 것은 다르다. 세 확인 모두 값을 찍지 않고 길이만 본다.

kubectl -n keycloak-lab describe secret keycloak-lab-secrets

실측(observed) — 아래쪽 Data 절만 옮겼다

Data
====
KC_BOOTSTRAP_ADMIN_PASSWORD:  19 bytes
POSTGRES_PASSWORD:            22 bytes
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
  -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d | wc -c
kubectl -n keycloak-lab exec keycloak-0 -- \
  sh -c 'echo "길이=${#KC_BOOTSTRAP_ADMIN_PASSWORD}"'
길이=19

어디를 봐야 하는가 — ① 은 Data 절의 키 이름과 그 옆의 바이트 수 두 칸, ② 와 ③ 은 숫자 하나다. 세 수가 서로 같은가.

이 결과가 의미하는 것describe 는 값을 절대 찍지 않고 길이만 보여 주므로 키 목록 확인과 「비어 있지 않은가」 확인이 한 명령으로 끝난다. 세 수가 같으면 Secret 에서 파드 환경변수까지 이어졌다. 길이=0 이면 Secret 에는 있는데 이 파드가 그것을 안 받은 것이라, envFrom 이나 valueFrom 을 빠뜨렸거나 파드가 Secret 을 고치기 전에 떠서 옛 값을 들고 있다. 환경변수로 주입한 Secret 은 값을 바꿔도 파드를 다시 만들기 전까지 갱신되지 않는다. 바이트 수가 뜻밖에 크면, 이를테면 20 이어야 할 것이 21 이면 만들면서 개행이 같이 들어간 것이고 증상은 「비밀번호가 틀렸다」로 나온다. 매니페스트가 기대하는 키 이름이 하나라도 다르면 파드는 CreateContainerConfigError 로 멈추고, 까닭은 describe pod 의 Events 에 키 이름까지 적혀 나온다.

-o yaml 로 보지 않는다. base64 는 암호화가 아니라 인코딩이라 화면과 스크롤백과 화면 공유와 터미널 로그에 값이 그대로 찍힌다.

어느 환경변수가 어느 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
KC_HOSTNAME
KC_HOSTNAME_STRICT
KC_PROXY_HEADERS
KC_HTTP_ENABLED
KC_HEALTH_ENABLED
KC_METRICS_ENABLED
JAVA_OPTS_KC_HEAP
KC_BOOTSTRAP_ADMIN_USERNAME
KC_BOOTSTRAP_ADMIN_PASSWORD	keycloak-lab-secrets

열세 줄이 나오고 오른쪽이 채워진 줄은 둘이다(2026-09-17, observed). 처음에는 앞의 네 줄만 실었는데 그러면 KC_BOOTSTRAP_ADMIN_PASSWORD 가 Secret 에서 오는 것이 안 보인다.

오른쪽 칸이 채워진 줄만 Secret 을 참조한다. 비밀이어야 할 변수의 오른쪽이 비어 있으면 그 값은 매니페스트에 평문으로 적혀 있다는 뜻이고, 그 파일은 대개 git 에 들어간다.

확인 ④ Service 뒤에 파드가 있나

무엇을 확인하는가 — Service 가 있어도 셀렉터가 안 맞으면 뒤가 비어 있다.

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

어디를 봐야 하는가 — 쉼표로 갈린 주소가 몇 개인가, 그 IP 들이 파드 IP 와 같은가, 포트 번호가 컨테이너가 실제로 듣는 포트인가.

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

목록으로 보려면 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 이고 실행하면 경고가 나온다(external) — 이 실험대의 k3s 는 v1.36.4+k3s1 이라 해당한다. 옛 문서와 블로그에 그 형태가 많다.

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

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

확인 ⑤ PVC 가 실제로 붙었나

무엇을 확인하는가 — 볼륨이 실제로 잡혔는지.

kubectl -n keycloak-lab get pvc

어디를 봐야 하는가 — STATUS 칸이 Bound 인가 Pending 인가, VOLUME 칸이 비어 있는지, STORAGECLASS 칸이 local-path 인가.

이 실험대에는 파드 번호가 붙은 PVC 가 없다. 「StatefulSet 이면 PVC 이름 끝에 파드 번호가 붙는다」는 volumeClaimTemplates 를 쓰는 StatefulSet 의 이야기이고 이 매니페스트의 keycloak StatefulSet 에는 그 칸이 없다. PVC 는 PostgreSQL 이 쓰는 postgres-data 하나뿐이고 그것도 StatefulSet 이 아니라 별도 문서로 선언되어 Deployment 가 claimName 으로 가리킨다. 2026-09-17 에 매니페스트와 클러스터 양쪽에서 확인했다(observed).

NAME            STATUS   VOLUME                                     CAPACITY   STORAGECLASS
postgres-data   Bound    pvc-fe269834-3dd1-4b1f-82f6-a53413310a30   5Gi        local-path

그래서 Keycloak 파드는 볼륨을 안 갖는다. 세션이 어디 사는지를 묻는 이 실험대의 질문에서 그것이 중요하다 — 파드가 죽으면 그 안의 것은 사라지고 postgres-data 만 남는다.

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

kubectl -n keycloak-lab describe pvc

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

확인 ⑥ 클러스터가 묶였나 — 셋이 서로 다른 것을 말한다

무엇을 확인하는가 — 두 Keycloak 이 하나의 클러스터로 묶였는지. 셋이 각각 다른 시점을 말한다. 로그는 「그때 그렇게 보였다」이고, 테이블은 「지금 등록되어 있다」이며, 지표는 「지금 그 노드가 그렇게 안다」이다.

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 이 뷰 번호다. 뷰 번호는 멤버가 들고 날 때마다 올라간다.

이름 뒤에 (v=…) 가 붙어 나오기도 한다. 2026-09-17 에 새로 세운 클러스터에서 같은 줄을 뽑아 보니 이랬다(observed).

[keycloak-1-3159(v=16.0.12)|1] (2) [keycloak-1-3159(v=16.0.12), keycloak-0-13057(v=16.0.12)]

(v=16.0.12) 는 JGroups 가 붙인 판 번호이고, 볼 것은 그대로 (2) 와 이름 목록과 |1 이다. 맨 앞의 이름이 keycloak-0 이 아니어도 정상이다 — 맨 앞에는 코디네이터가 오고 먼저 뜬 쪽이 그 일을 맡는다. 이 판에서는 keycloak-1 이었다.

이름 목록의 keycloak-0-10001 은 파드 이름 그대로가 아니다. Infinispan 이 파드 이름 뒤에 임의의 접미사를 붙여 클러스터 노드 식별자로 쓰기 때문에, 아래 지표에 나오는 keycloak-0-46674 와는 keycloak-0 까지만 같다. 그래서 대조할 때 맞춰 보는 것은 접미사 앞의 파드 이름이다. Keycloak 만 StatefulSet 이라 그 앞부분이 고정이고, 덕분에 로그와 jgroups_ping 테이블과 지표를 같은 이름으로 견줄 수 있다.

이 결과가 의미하는 것(2) 면 이 노드는 상대를 봤다. (1) 이면 혼자 있다고 알고 있다. 뷰 번호가 계속 오르고 있으면 멤버가 붙었다 떨어지기를 반복하는 중이라, 「지금 2 다」라는 스냅숏보다 그 사실이 더 중요하다. 이 줄은 과거형이므로 지금 상태는 지표로 본다.

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

행이 몇 개인가, 그리고 ip 칸이 확인 ④ 에서 본 파드 IP 와 같은가를 본다. 이 표는 「등록되어 있다」이지 「서로 말이 통한다」가 아니다. 두 행이 다 있는데 로그가 (1) 이면 서로를 찾기는 했는데 7800 포트로 메시지가 안 가는 것이고, 이 실험대에서 실제로 그 일이 벌어졌다(observed). 옛 파드의 행이 남아 있을 수도 있으므로 IP 를 지금 파드와 대조한다.

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

실측(observed) — 줄바꿈 없는 JSON 한 줄에서 읽어낸 값이다

keycloak-1 → 2
keycloak-0 → 2

응답은 줄바꿈 없는 JSON 한 덩어리로 온다. data.result 배열에서 원소가 몇 개인가, 각 원소에서 metric 안의 node 라벨과 value 배열의 둘째 원소만 읽는다. 이 실험대에는 jq 가 없으므로 파서를 따로 짜지 말고 화면에 나온 JSON 을 그대로 읽는다. 원소가 하나뿐이면 나머지 노드의 스크레이프가 실패한 것이라 클러스터 문제가 아니라 관측 문제일 수 있다. 각 노드가 자기가 아는 멤버 수를 보고하므로 한 노드만 보면 분단을 놓친다 — 분단되면 한쪽은 2, 다른 쪽은 1 이 된다.

:::warning

Keycloak 컨테이너에는 curl 이 없다. 공식 이미지가 최소 구성이라 wgetnc 도 없고, 안에서 치면 command not found 와 exit code 127 로 끝난다.

:::

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

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 라벨이 어느 파드인가를 본다. 이 형태는 그 파드 하나에게 직접 물은 것이라 라벨의 노드 이름과 고른 파드가 반드시 일치한다. --rm 을 붙였으므로 파드는 끝나면 사라진다. 값이 안 나오고 연결 거부가 나면 관리 포트 9000 이 안 열렸다.

확인 ⑦ 밖에서 닿나

무엇을 확인하는가 — nginx 에서 Traefik, Ingress, Service 를 지나 파드까지 전부 이어졌는지.

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

어디를 봐야 하는가 — 코드 한 칸. 여기서 값만 뽑는 형태를 쓰는 것은 앞 두 단계에서 잰 것과 같은 명령으로 같은 값이 나오는지 비교하는 것이 목적이기 때문이다.

이 결과가 의미하는 것200 이면 2홉이 다 이어졌다. 502503 이면 뒤에서부터 되짚는다 — Ingress 가 있는지(확인 ①), Service 뒤에 파드가 있는지(확인 ④), 파드가 Ready 인지(확인 ②) 순서다. 처음 보는 오류라 헤더가 필요하면 읽는 형태로 바꾼다.

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

200 은 앞 단계를 끝냈을 때의 값이다. 인증서 단계를 건너뛰고 이 단계만 했다면 443 을 듣는 것이 없으므로 여기는 000 이다. 2026-09-17 에 그 상태에서 재 보니 이렇게 나왔다(observed).

curl: (7) Failed to connect to auth.hyeonworks.com:443 after 2 ms: Could not connect to server

그때도 이 단계가 세운 것까지는 잴 수 있다. TLS 를 빼고 Host 헤더를 실어 80 으로 치면 nginx → Traefik → Ingress → Service → 파드가 이어졌는지가 그대로 나온다. 둘 다 200 이었다(observed).

curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: auth.hyeonworks.com' http://192.168.122.10/realms/master
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: auth.hyeonworks.com' http://192.168.122.11/realms/master

앞엣것은 엣지 nginx 를 지나고 뒤엣것은 Traefik 에 바로 친다. 둘이 같으면 남은 것은 TLS 뿐이다.

확인 ⑧ 관리 콘솔에 로그인된다

무엇을 확인하는가 — 가이드가 적은 「이 단계가 끝나면」의 앞 절반.

브라우저로 https://auth.hyeonworks.com/admin 에 들어가 관리자로 로그인한다. 비밀번호는 확인 ③ 의 Secret 에 있고, 이 문서는 그 값을 적지 않는다 — 이 실험대의 확인은 전부 길이까지만 본다.

세션이 실제로 어디 저장되는지까지 보려면 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 를 로그인 전과 후에 두 번 재서 그 차이를 본다. 한 번만 재면 아무것도 알 수 없다. 로그인 뒤 수가 늘면 세션이 DB 에 남는 것이고, 안 늘면 메모리에만 있다. 메모리에만 있으면 파드를 재시작하는 순간 세션이 사라지고, DB 에 있으면 살아남는다.

통과 조건을 한 번에 다시 본다

무엇 명령 통과
롤아웃 kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s complete · 파드 2
Service 뒤 kubectl -n keycloak-lab describe svc keycloak | grep -i endpoints 주소 두 개
클러스터 … query=vendor_cluster_size 원소 둘 · 값 둘 다 2
밖에서 curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master 200

막히면

가이드 05 가 순서를 정해 두었다. 로그부터 보지 않는다.

① 이벤트부터. 스케줄링과 이미지와 볼륨 실패가 여기 나온다.

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

맨 아랫줄부터 거꾸로 읽는다. --sort-by 를 준 까닭이 그것이다. TYPE 이 Warning 인 줄, REASON 칸, 그리고 OBJECT 칸이 어느 파드인가를 본다. 이벤트는 기본 한 시간만 남으므로 아무것도 없다고 문제가 없는 것은 아니다. REASON 하나가 다음 행동을 정한다 — FailedScheduling 은 노드에 자리가 없는 것이라 파드가 아니라 클러스터를 봐야 하고, ErrImagePull 은 이미지 이름 문제라 로그를 볼 것도 없으며, BackOff 는 컨테이너가 떴다가 죽는 중이라 ③ 으로 간다.

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

kubectl -n keycloak-lab describe pod keycloak-0

위에서부터 세 곳이다. Conditions 절에서 ReadyFalse 인가, 컨테이너 절의 StateLast State 와 그 안의 Exit Code, 그리고 맨 아래 Events. Exit Code 는 그것만으로 말이 된다 — 137 은 OOM 이나 강제 종료, 1 은 애플리케이션이 스스로 끝낸 것, 127 은 명령을 못 찾았다는 뜻이다. 137 이면 로그에 아무 단서도 없을 수 있어 메모리 한도를 본다. ReadyFalse 이고 컨테이너는 살아 있으면 readiness 프로브 문제이므로 확인 ④ 로 돌아간다.

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

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
증상 어디를 보나
파드가 Pending describe pod 의 Events — 스케줄 불가 사유
ImagePullBackOff 이미지 이름과 태그. 자체 빌드면 두 노드 모두에 반입했는가
CrashLoopBackOff logs --previous
Running 인데 0/1 readiness 프로브 실패. describe 의 Conditions
밖에서 502 Ingress 에서 Service 로, Service 에서 Endpoints 로 뒤를 본다
클러스터가 1로 보임 7800 이 막혔거나 디스커버리 실패. 확인 ⑥ 의 셋 다
AlreadyExists kubectl create namespace 를 먼저 쳤다. 매니페스트가 만든다
error: the path ... does not exist 저장소 루트가 아닌 곳에서 쳤다

무엇이 관측이고 무엇이 아닌가

  • (observed) 2026-09-11 의 ReplicaSet 과 파드 이름과 해시, Secret 의 19 와 22 bytes 와 주입된 길이 19 와 복호 길이 22, Endpoints 두 개와 EndpointSlice 의 ready 두 줄, ISPN000094 뷰 줄, vendor_cluster_size 가 둘 다 2, 임시 파드가 읽은 2.0, Keycloak 이미지에 curl 이 없다는 것과 exit code 127.
  • (observed) 디스커버리 테이블에는 둘 다 있는데 메시지가 안 가는 상태를 실제로 겪었다.
  • (external) kubectl get endpoints 의 v1.33 deprecation 은 쿠버네티스 쪽 규격이고 이 실험대가 잰 값이 아니다.
  • (external) Infinispan 이 파드 이름에 임의의 접미사를 붙여 클러스터 노드 식별자로 쓴다는 것은 Infinispan 의 동작이다. 이 기록에 실린 keycloak-0-10001keycloak-0-46674 가 그렇게 생긴 이름이다(observed).
  • (unknown) keycloak-cluster.yaml 원문은 source/ 에 없다. 파드 자원 한도와 프로브 설정과 persistent-user-sessions 설정값은 아직 이 기록으로 옮기지 않았다.
  • (unknown) get allget deployget pvcdescribe pvc 와 디스커버리 테이블 조회와 get eventsdescribe podlogs 의 출력은 가이드가 봐야 할 줄만 적었고 값이 남아 있지 않다.
  • (unknown) 원본 가이드 05 에 되돌리는 절차가 없다. local-path PVC 가 노드 디스크에 남긴 데이터가 네임스페이스를 지울 때 어떻게 되는지도 확인하지 않았다.
  • (unknown) 이 단계에는 「이 실험대는 이렇게 했다」와 「따라 하는 사람은」 두 갈래가 없다. 05 는 파일을 직접 쓰지 않고 저장소의 매니페스트를 적용한다.
  • (observed) 2026-09-17 에 갓 세운 클러스터에서 이 절차를 처음부터 다시 밟았다. 같은 값이 다시 나온 것 — postgres-7b474b88c8 이라는 ReplicaSet 해시, Secret 의 19·22 bytes 와 주입된 길이 19·복호 길이 22, Endpoints 두 개와 ready 두 줄, jgroups_ping 2 행, 임시 파드가 읽은 vendor_cluster_size … 2.0, Keycloak 이미지에 curl 이 없다는 것과 exit code 127, kubectl get endpoints 의 deprecation 경고. 파드 IP 와 Infinispan 접미사만 달랐다 — 그것들은 매번 새로 붙는다.
  • (observed) 같은 날 잰 것 가운데 문서와 달랐던 것 — rollout statusWaiting for … 를 찍는다는 것, PVC 가 postgres-data 하나라는 것, 환경변수 목록이 열세 줄이라는 것, ISPN000094 이름에 (v=16.0.12) 가 붙는다는 것, 인증서 단계를 건너뛰면 밖에서가 000 이라는 것.
  • (observed) keycloak-cluster.yamlkeycloak-pattern @ 9465582b 에서 읽었다. 이미지 태그는 postgres:16-alpinequay.io/keycloak/keycloak:26.7.0 이다.