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 2109f726fe feat(pipeline): keycloak-session-store 25편·virtualization 59편을 S3→S5→S6 으로 돌린다
기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만
있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문
종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다.

SSOT 결함 둘을 고쳤다.

- kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이
  「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽
  `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다.
- virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데
  원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다.

기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은
둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더
든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다).

계약을 셋 고쳤다.

- kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/**
  28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는
  것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온
  것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수
  대조했을 때 가장 가까운 커밋도 28개가 어긋났다).
- virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고
  재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다.
- kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고
  넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다.

style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져
(실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고,
engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고
있었다(Question 기록에서 11.94 → 3.86).

verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK.

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

33 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 이다.

이 단계가 세우는 것

가이드 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 StorageClass local-path, 이름 끝에 파드 번호가 붙는다
Ingress HOSTS 가 auth.hyeonworks.com
클러스터링 JGroups — 디스커버리 테이블 jgroups_ping, 메시지 포트 7800
관리 포트 9000/metrics 가 거기 있다

판 번호는 여기 없다(unknown). 가이드 05 는 Keycloak 과 PostgreSQL 의 이미지 태그를 적지 않았고 keycloak-cluster.yaml 원문은 반입되지 않았다. 가이드 안에 태그가 찍힌 이미지는 아래 임시 파드의 curlimages/curl:8.11.1 하나뿐이다.

전제와 되돌리기

전제는 한 줄이다 — 앞 단계까지 끝나 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' https://auth.hyeonworks.com/

어디를 봐야 하는가 — 앞 단계에서 잰 404 tls=0 이 그대로인가. 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

예상 결과

partitioned roll out complete: 2 new pods have been updated...

이 명령은 끝날 때까지 아무것도 안 찍고 멈춰 있다. 그 침묵이 정상이고, 마지막 한 줄에서 complete 라는 낱말과 파드 개수 2 를 본다. 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

오른쪽 칸이 채워진 줄만 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 인가. StatefulSet 이면 PVC 이름 끝에 파드 번호가 붙어 있어 어느 파드 것인지 바로 보인다.

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

이름 목록의 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' https://auth.hyeonworks.com/realms/master
200

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

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

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

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

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

브라우저로 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 원문이 반입되지 않아 Keycloak 과 PostgreSQL 의 이미지 태그, 파드 자원 한도, 프로브 설정, persistent-user-sessions 설정값은 대조하지 못했다. 위에 적은 객체 이름과 해시와 바이트 수는 돌고 있던 실험대에서 읽은 것이고, 매니페스트가 그것들을 어떤 값으로 선언했는지는 여기서 확인할 수 없다.
  • (unknown) get allget deployget pvcdescribe pvc 와 디스커버리 테이블 조회와 get eventsdescribe podlogs 의 출력은 가이드가 봐야 할 줄만 적었고 값이 남아 있지 않다.
  • (unknown) 원본 가이드 05 에 되돌리는 절차가 없다. local-path PVC 가 노드 디스크에 남긴 데이터가 네임스페이스를 지울 때 어떻게 되는지도 확인하지 않았다.
  • (unknown) 이 단계에는 「이 실험대는 이렇게 했다」와 「따라 하는 사람은」 두 갈래가 없다. 05 는 파일을 직접 쓰지 않고 저장소의 매니페스트를 적용한다.
  • (unknown) 만드는 명령은 구축할 때 친 것을 옮긴 것이라 다시 쳐서 같은 상태가 되는지는 확인되지 않았다.