# 05 — Keycloak 2노드 + PostgreSQL ## 이 단계가 끝나면 `https://auth.hyeonworks.com` 에서 관리 콘솔에 로그인되고, 두 Keycloak 이 하나의 클러스터로 보인다. ## 전제 [04](../04-tls/) 까지 끝나 `https://` 가 열린다. --- ## 1. 매니페스트를 적용한다 **하기** ```bash kubectl create namespace keycloak-lab kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml ``` **확인** — 적용이 끝날 때까지 기다린다 ```bash 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. 무엇이 만들어졌나 **확인** — 이 네임스페이스에 무엇이 서 있는가 ```bash kubectl -n keycloak-lab get all ``` **어디를 봐야 하는가** — 종류별로 묶여 나오는 왼쪽 이름 열을 훑고, 파드 줄에서는 **READY 칸의 `1/1`** 과 **RESTARTS 칸**을 본다. RESTARTS 가 0 이 아니면 지금은 `Running` 이어도 한 번 죽었다 살아난 것이라, 3번의 `logs --previous` 를 볼 이유가 된다. **이 결과가 의미하는 것** — 여기 보이는 것이 워크로드의 전부다. 그런데 `all` 은 이름과 달리 전부는 아니다 — **Secret·ConfigMap·PVC·Ingress 는 안 나온다.** 이 넷이 빠졌다는 사실을 모르고 「다 만들어졌다」고 판정하는 것이 흔한 오독이라, 한 번 더 친다. ```bash 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 을 만들고 그것이 파드를 만든다.** 이 사슬 어디서 끊겼는지가 진단의 출발점이다. **확인** — 사슬 어디까지 갔는가 ```bash 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 에 무슨 키가, 얼마만큼 들어 있나 — **값은 찍지 않는다** ```bash 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`](../../experiment-d3-secret-management.md) **확인 ②** 특정 키 하나를 따져 볼 때 — **길이만** `describe` 가 보여 주는 바이트 수는 base64 를 푼 뒤의 길이다. 어떤 키 하나가 의심스러워 다시 잴 때만 이 형태를 쓴다. ```bash 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 안에 무엇이 있나**이고, 파드가 그것을 받았는지는 아직 모른다. **확인 ③** 파드 안에 주입됐나 — 여기가 진짜다 ```bash 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 에서 왔는지**도 볼 수 있다. ```bash 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 가 있어도 **셀렉터가 안 맞으면 뒤가 비어 있다.** 이때 증상은 「연결은 되는데 응답이 없다」라 원인을 찾기 어렵다. **확인** — 실무자가 가장 자주 쓰는 형태 ```bash 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** 를 쓴다. ```bash 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 > ``` > 옛 문서와 블로그에 이 형태가 많으니 주의한다. 준비 상태까지 함께 보려면 이렇게 뽑는다. ```bash 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` 로 충분하다. **이 결과가 의미하는 것** — `ready` 가 `false` 면 파드는 있는데 **readiness 프로브를 통과하지 못한** 것이라, Service 가 그 파드로 트래픽을 보내지 않는다. 파드 목록에서는 `Running` 으로 보이므로 `get pods` 만 봐서는 알 수 없다 — `0/1` 인지 `1/1` 인지가 같은 사실을 말해 준다. **비어 있으면** 셀렉터와 파드 라벨이 안 맞는 것이다. ```bash kubectl -n keycloak-lab get svc keycloak -o jsonpath='{.spec.selector}'; echo kubectl -n keycloak-lab get pods --show-labels ``` ### 2-5. PVC 가 실제로 붙었나 **확인** — 볼륨이 실제로 잡혔는가 ```bash 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 의 이벤트에 적혀 있다. ```bash kubectl -n keycloak-lab describe pvc # 이름을 안 주면 전부 나온다 ``` 각 PVC 절의 맨 아래 `Events` 에 `waiting for first consumer` 인지 `no persistent volumes available` 인지가 적혀 있고, 둘은 대응이 다르다 — 앞엣것은 파드를 고치는 문제, 뒤엣것은 저장소를 고치는 문제다. --- ## 3. 안 뜰 때 — 순서가 있다 **① 이벤트부터.** 로그보다 먼저다. 스케줄링·이미지·볼륨 실패가 여기 나온다. ```bash 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.** 그 파드에 한정된 이벤트와 상태를 함께 본다. ```bash kubectl -n keycloak-lab describe pod keycloak-0 ``` **어디를 봐야 하는가** — 위에서부터 세 곳이다. `Conditions` 절에서 `Ready` 가 `False` 인가, 컨테이너 절의 `State`/`Last State` 와 그 안의 **`Exit Code`**, 그리고 맨 아래 `Events`. Exit Code 는 그것만으로 말이 된다 — `137` 은 OOM 이나 강제 종료, `1` 은 애플리케이션이 스스로 끝낸 것, `127` 은 명령을 못 찾은 것이다. **이 결과가 의미하는 것** — Exit Code 가 `137` 이면 로그에는 아무 단서도 없을 수 있다(맞아 죽은 쪽은 유언을 못 남긴다) — 메모리 한도를 본다. `Ready` 만 `False` 이고 컨테이너는 살아 있으면 readiness 프로브 문제이므로 2-4 로 돌아간다. **③ 로그.** 컨테이너가 떴는데 죽는 경우다. ```bash kubectl -n keycloak-lab logs keycloak-0 kubectl -n keycloak-lab logs keycloak-0 --previous # 재시작 직전 로그 ``` **어디를 봐야 하는가** — 첫 명령에서는 **마지막 줄들**, 둘째 명령에서는 스택 트레이스의 **맨 윗줄**(가장 안쪽 예외가 아니라 최초 원인 줄)이다. Keycloak 은 기동에 성공하면 `Keycloak ... started in` 한 줄을 남기므로, 그 줄이 있는지 없는지가 「기동 중」과 「기동 실패」를 가른다. **이 결과가 의미하는 것** — `--previous` 가 중요하다. CrashLoopBackOff 면 지금 컨테이너는 방금 뜬 것이라 **죽은 이유는 이전 컨테이너 로그에 있다.** `--previous` 가 `not found` 를 내면 아직 한 번도 재시작하지 않은 것이고, 그러면 지금 로그가 곧 전부다. **④ 그래도 모르면 안에서 본다.** ```bash kubectl -n keycloak-lab exec -it keycloak-0 -- sh ``` --- ## 4. 클러스터가 형성됐는지 확인한다 파드가 둘 다 `Running` 인 것과 **하나의 클러스터로 묶인 것**은 다르다. **확인 ①** 로그 ```bash 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` 이 아무것도 못 찾으면 클러스터링을 아직 시작도 못 한 것이니 로그를 통째로 본다. **확인 ②** 디스커버리 테이블 ```bash 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` 이 없다.** 공식 이미지가 최소 구성이라 `wget` 도 `nc` 도 없다. 안에서 치면 이렇게 된다. ``` sh: line 1: curl: command not found command terminated with exit code 127 ``` 그래서 밖에서 물어본다. **Prometheus 에 묻는 것이 가장 짧다.** ```bash 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 가 아직 없다면 임시 파드를 띄운다. ```bash 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홉을 다 지나 파드까지 닿는가 ```bash 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) 순서다. 처음 보는 오류라 헤더가 필요하면 값만 뽑는 형태를 버리고 읽는 형태로 바꾼다. ```bash 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 를 직접 본다. ```bash 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` 인 행의 `count`. **로그인 전과 후에 두 번 재서 그 수의 차이**를 본다. 한 번만 재면 아무것도 알 수 없다. 행이 아예 없으면(`0 rows`) 표는 있는데 비어 있는 것이다. **이 결과가 의미하는 것** — 로그인 뒤 수가 늘면 세션이 DB 에 남는 것 (`persistent-user-sessions` 켜짐)이고, 안 늘면 메모리에만 있는 것이다. 그 차이가 A층 결론 전체를 뒤집는다 — 메모리에만 있으면 파드를 재시작하는 순간 세션이 사라지고, DB 에 있으면 살아남는다. [A-7](../../experiment-a7-volatile-comparison.md)