# 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... ``` `rollout status` 는 **끝날 때까지 블록**한다. `get pods` 를 반복해서 보는 것보다 이게 낫다. --- ## 2. 리소스가 제대로 만들어졌는지 — 층별로 본다 `kubectl get pods` 만 보면 놓치는 것이 많다. **위에서 아래로** 확인한다. ### 2-1. 무엇이 만들어졌나 **확인** ```bash kubectl -n keycloak-lab get all ``` `all` 은 이름과 달리 전부는 아니다 — Secret·ConfigMap·PVC·Ingress 는 안 나온다. ```bash kubectl -n keycloak-lab get secret,configmap,pvc,ingress ``` ### 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 이 여러 개인 것은 정상이다.** 배포할 때마다 새로 만들고 옛것은 `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 에 키가 있나 — **값은 찍지 않는다** ```bash 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 ``` **확인 ②** 값이 비어 있지 않나 — **길이만** ```bash kubectl -n keycloak-lab get secret keycloak-lab-secrets \ -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d | wc -c ``` ``` 22 ``` **확인 ③** 파드 안에 주입됐나 — 여기가 진짜다 ```bash kubectl -n keycloak-lab exec keycloak-0 -- \ sh -c 'echo "길이=${#KC_BOOTSTRAP_ADMIN_PASSWORD}"' ``` ``` 길이=19 ``` **어느 환경변수가 어느 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 에서 온 것만 오른쪽에 이름이 있다 ``` > **값을 그대로 찍지 않는 습관.** `-o yaml` 은 base64 를 그대로 보여 주고 > 그건 암호화가 아니다. 터미널 스크롤백·화면 공유·로그에 남는다. > D-3 이 잰 것이 이것이다 — [`experiment-d3-secret-management.md`](../../experiment-d3-secret-management.md) ### 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 ``` 목록으로 보려면 **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 ``` `ready` 가 `false` 면 파드는 있는데 **readiness 프로브를 통과하지 못한** 것이라, Service 가 그 파드로 트래픽을 보내지 않는다. **비어 있으면** 셀렉터와 파드 라벨이 안 맞는 것이다. ```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 ``` `Pending` 이면 StorageClass 가 없거나 노드에 자리가 없다. **`local-path` 는 파드가 스케줄될 때까지 기다린다**(WaitForFirstConsumer)므로, 파드가 안 뜨면 PVC 도 `Pending` 인 것이 정상이다. 둘이 서로를 기다리는 것처럼 보이지만 파드 쪽 원인을 먼저 본다. --- ## 3. 안 뜰 때 — 순서가 있다 **① 이벤트부터.** 로그보다 먼저다. 스케줄링·이미지·볼륨 실패가 여기 나온다. ```bash kubectl -n keycloak-lab get events --sort-by=.lastTimestamp | tail -20 ``` **② describe.** 그 파드에 한정된 이벤트와 상태를 함께 본다. ```bash kubectl -n keycloak-lab describe pod keycloak-0 ``` **③ 로그.** 컨테이너가 떴는데 죽는 경우다. ```bash kubectl -n keycloak-lab logs keycloak-0 kubectl -n keycloak-lab logs keycloak-0 --previous # 재시작 직전 로그 ``` `--previous` 가 중요하다. CrashLoopBackOff 면 지금 컨테이너는 방금 뜬 것이라 **죽은 이유는 이전 컨테이너 로그에 있다.** **④ 그래도 모르면 안에서 본다.** ```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)` 가 멤버 수다. **확인 ②** 디스커버리 테이블 ```bash 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 에 묻는 것이 가장 짧다.** ```bash kubectl -n observability exec deploy/prometheus -- \ wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' ``` ``` keycloak-1 → 2 keycloak-0 → 2 ``` 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 ``` > **두 값이 다를 수 있다.** 각 노드가 자기가 아는 멤버 수를 보고하므로, > 분단되면 한쪽은 2 다른 쪽은 1 이 된다. **한 노드만 보면 분단을 놓친다.** > **셋이 다른 것을 본다.** 로그는 「그때 그렇게 보였다」이고, 테이블은 > 「지금 등록되어 있다」이며, 지표는 「지금 그 노드가 그렇게 안다」이다. > A-1 에서 이 셋이 갈렸다 — 테이블에는 둘 다 있는데 메시지는 안 갔다. --- ## 5. 밖에서 닿는지 **확인** ```bash 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 를 직접 본다. ```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'` 이 온라인 세션이다. 로그인하고 이 수가 늘면 `persistent-user-sessions` 가 켜져 있는 것이고, 안 늘면 메모리에만 있는 것이다. 그 차이가 A층 결론 전체를 뒤집는다 — [A-7](../../experiment-a7-volatile-comparison.md)