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>
10 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...
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
ready 가 false 면 파드는 있는데 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 이 없다. 공식 이미지가 최소 구성이라
wget 도 nc 도 없다. 안에서 치면 이렇게 된다.
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