docs(guides): hands-on guides for the seven setup stages

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>
This commit is contained in:
DongHyeonka
2026-09-07 16:48:10 +09:00
co-authored by Claude Opus 5
parent cdac9b8178
commit 88b7bd4bf0
8 changed files with 1453 additions and 0 deletions
+330
View File
@@ -0,0 +1,330 @@
# 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)