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>
331 lines
10 KiB
Markdown
331 lines
10 KiB
Markdown
# 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)
|