기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만 있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문 종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다. SSOT 결함 둘을 고쳤다. - kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이 「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽 `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다. - virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데 원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다. 기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은 둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더 든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다). 계약을 셋 고쳤다. - kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/** 28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는 것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온 것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수 대조했을 때 가장 가까운 커밋도 28개가 어긋났다). - virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고 재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다. - kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고 넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다. style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져 (실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고, engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고 있었다(Question 기록에서 11.94 → 3.86). verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
521 lines
33 KiB
Markdown
521 lines
33 KiB
Markdown
---
|
|
id: 7b113a04-180a-40ec-9270-033531c22221
|
|
kind: SETUP
|
|
slug: keycloak-two-nodes-and-postgres-on-k3s
|
|
title: Keycloak 2노드와 PostgreSQL 을 k3s 에 올리고 클러스터가 묶였는지 확인한다
|
|
topic: lab-environment-build
|
|
topicName: 실험대 환경 구성
|
|
project: virtualization
|
|
status: 게시 전
|
|
studio: "https://hyeonworks.com/studio/documents/7b113a04-180a-40ec-9270-033531c22221/edit"
|
|
pinnedVersions:
|
|
- name: k3s
|
|
version: v1.36.4+k3s1
|
|
- name: curlimages/curl
|
|
version: 8.11.1
|
|
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
|
source:
|
|
- final/document.md#191-단계-05-keycloak-2노드와-postgresql
|
|
- final/document.md#184-이-부의-출처와-범위
|
|
---
|
|
|
|
# Keycloak 2노드와 PostgreSQL 을 k3s 에 올리고 클러스터가 묶였는지 확인한다
|
|
|
|
매니페스트 한 장을 `apply` 해 네임스페이스 `keycloak-lab` 에 Keycloak 두 대와 PostgreSQL 하나를 세우는 절차다. 세우는 명령은 두 줄이고, 나머지는 그 둘이 하나의 클러스터로 묶였는지 확인하는 명령이다.
|
|
|
|
## 관계
|
|
|
|
- **k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다**
|
|
여기 적힌 `kubectl` 이 도는 클러스터를 그 단계가 세운다. INTERNAL-IP 가 어긋난 채로 왔으면 여기서 Endpoints 가 한 줄만 나온다.
|
|
- **Prometheus 와 Grafana 를 올려 클러스터 안을 밖에서 본다**
|
|
클러스터 크기를 지표로 묻는 확인 명령이 그 스택을 전제한다. 아직 없으면 임시 파드를 띄워 같은 값을 받는다.
|
|
- **DNS-01 으로 와일드카드 인증서를 받고 갱신이 서빙까지 닿게 한다**
|
|
밖에서 도메인으로 `200` 을 받는 마지막 확인이 그 단계가 올린 443 을 지난다.
|
|
- **도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다**
|
|
`kubectl get all` 이 이름과 달리 전부를 세지 않고, 로그와 테이블과 지표가 각각 다른 시점을 말한다. 그 구분이 이 단계의 판정을 셋으로 가른다.
|
|
- **가까운 층부터 한 층씩 건너뛰며 확인한다 — 층마다 성공 신호가 다르다**
|
|
밖에서 502 가 나올 때 Ingress 에서 Service 로, Service 에서 Endpoints 로 되짚는 순서가 그 기준에서 나온다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
## 읽기 전에 — 어디서 치는가
|
|
|
|
이 단계는 앞의 셋보다 단순하다. 전부 `[lab host]` 에서 치고, 저장소 루트에서 친다. 마지막 로그인 확인만 브라우저다.
|
|
|
|
| 번호 | 무엇 | 어디서 |
|
|
|---|---|---|
|
|
| 1 | 매니페스트 적용 | `[lab host]` — `~/workspace/keycloak-pattern` 안에서 |
|
|
| 2 | 파드 둘이 설 때까지 기다리기 | `[lab host]` |
|
|
| 판정 | 리소스 · 클러스터 · 밖에서 | `[lab host]` |
|
|
| 판정 | 관리 콘솔 로그인 | 브라우저 |
|
|
|
|
`kubectl` 이 lab host 에서 도는 까닭은 앞 단계에 있다. kubeconfig 를 게스트에서 호스트로 가져다 두었으므로 게스트에 들어갈 일이 없다. `deploy/...` 로 시작하는 상대경로는 저장소 루트 기준이라, 다른 디렉터리에서 치면 `error: the path ... does not exist` 로 막힌다.
|
|
|
|
이 단계에는 손으로 쓰는 파일이 없다. 세우는 것이 전부 매니페스트 한 장에 들어 있고, 그 원문은 저장소의 `deploy/lab/k8s/keycloak-cluster.yaml` 이다.
|
|
|
|
## 이 단계가 세우는 것
|
|
|
|
가이드 05 의 「이 단계가 끝나면」은 두 줄이다.
|
|
|
|
> `https://auth.hyeonworks.com` 에서 관리 콘솔에 로그인되고, 두 Keycloak 이
|
|
> 하나의 클러스터로 보인다.
|
|
|
|
**두 마디가 따로다.** 「로그인된다」는 밖에서 잰 `200` 과 브라우저이고, 「하나의 클러스터로 보인다」는 아래 세 확인이다. 파드가 둘 다 `Running` 인 것과 하나의 클러스터로 묶인 것은 다르다.
|
|
|
|
| 무엇 | 값 |
|
|
|---|---|
|
|
| 네임스페이스 | `keycloak-lab` — 매니페스트 첫 문서가 `kind: Namespace` 라 `apply` 가 같이 만든다 |
|
|
| Keycloak | StatefulSet `keycloak` — 파드 `keycloak-0` · `keycloak-1` |
|
|
| PostgreSQL | Deployment `postgres` — ReplicaSet `postgres-7b474b88c8` |
|
|
| Service | `keycloak` → Endpoints `10.42.0.67:8080,10.42.1.155:8080` |
|
|
| Secret | `keycloak-lab-secrets` — `KC_BOOTSTRAP_ADMIN_PASSWORD` 19 bytes · `POSTGRES_PASSWORD` 22 bytes |
|
|
| PVC | StorageClass `local-path`, 이름 끝에 파드 번호가 붙는다 |
|
|
| Ingress | HOSTS 가 `auth.hyeonworks.com` |
|
|
| 클러스터링 | JGroups — 디스커버리 테이블 `jgroups_ping`, 메시지 포트 7800 |
|
|
| 관리 포트 | `9000` — `/metrics` 가 거기 있다 |
|
|
|
|
**판 번호는 여기 없다**(unknown). 가이드 05 는 Keycloak 과 PostgreSQL 의 이미지 태그를 적지 않았고 `keycloak-cluster.yaml` 원문은 반입되지 않았다. 가이드 안에 태그가 찍힌 이미지는 아래 임시 파드의 `curlimages/curl:8.11.1` 하나뿐이다.
|
|
|
|
## 전제와 되돌리기
|
|
|
|
전제는 한 줄이다 — 앞 단계까지 끝나 `https` 가 열린다.
|
|
|
|
**되돌리는 절차는 원본 가이드 05 에 없다**(unknown). `kubectl delete -f` 도, 지우는 순서도 적혀 있지 않다. 이 단계가 클러스터에 남기는 것은 `keycloak-lab` 네임스페이스와 그 안의 전부이고, 그중 PVC 는 성격이 다르다 — StorageClass 가 `local-path` 라 데이터가 파드가 스케줄된 그 노드의 디스크에 놓인다. 네임스페이스를 지울 때 그 디렉터리가 어떻게 되는지는 가이드가 적지 않았고 이 실험대도 확인하지 않았다.
|
|
|
|
## 세우기 전에 먼저 본다
|
|
|
|
두 확인은 아무것도 바꾸지 않는다. 하나는 명령이 파일을 찾을 수 있는 곳인지 보고, 하나는 이 이름이 아직 Keycloak 이 아님을 확인한다.
|
|
|
|
### 확인 ① 지금 이 디렉터리에서 매니페스트가 보이는가
|
|
|
|
```bash label="[lab host] 저장소 루트로 가서 매니페스트를 본다"
|
|
cd ~/workspace/keycloak-pattern
|
|
ls deploy/lab/k8s/
|
|
```
|
|
|
|
**어디를 봐야 하는가** — `keycloak-cluster.yaml` 과 `observability.yaml` 이 보이는가.
|
|
|
|
**이 결과가 의미하는 것** — 보이면 아래 `kubectl apply -f deploy/...` 가 파일을 찾는다. 안 보이면 첫 단계의 저장소 받기로 돌아간다 — 한 번 세웠다 철거한 뒤에는 VM 과 디스크만 지워지고 저장소는 각자 관리라 이 디렉터리가 없는 상태가 흔하다.
|
|
|
|
### 확인 ② 이 이름이 아직 Keycloak 이 아니다
|
|
|
|
```bash label="[lab host] 앞 단계에서 잰 값을 그대로 다시 잰다"
|
|
curl -s -o /dev/null -w '%{http_code} tls=%{ssl_verify_result}\n' https://auth.hyeonworks.com/
|
|
```
|
|
|
|
**어디를 봐야 하는가** — 앞 단계에서 잰 `404 tls=0` 이 그대로인가. `tls=0` 이 아니면 이 단계를 시작할 때가 아니라 앞 단계로 돌아갈 때다.
|
|
|
|
**이 결과가 의미하는 것** — 가이드가 그 값 옆에 한 줄을 적어 두었다 — 앞의 `404` 는 이 단계 이후에 `200` 으로 바뀐다. 지금 재 두면 아래의 `200` 이 이 단계가 만든 변화인지가 분명해진다.
|
|
|
|
## 실행 절차
|
|
|
|
### 1. 매니페스트 한 장을 적용한다
|
|
|
|
**목적** — 네임스페이스부터 Ingress 까지 한 파일로 세운다.
|
|
|
|
```bash label="[lab host] ① 매니페스트가 있는 저장소 루트로 간다"
|
|
cd ~/workspace/keycloak-pattern
|
|
```
|
|
|
|
```bash label="[lab host] ② 매니페스트를 적용한다"
|
|
kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
|
|
```
|
|
|
|
**예상 결과** — 만든 객체가 줄마다 찍힌다. 파드가 서는 것은 그다음이라 이 출력만으로 끝났다고 판정하지 않는다.
|
|
|
|
**왜 필요한가** — 네임스페이스를 따로 만들지 않는다. 매니페스트 첫 문서가 `kind: Namespace` 라 `apply` 가 같이 만들고, `kubectl create namespace` 를 먼저 치면 두 번째 실행부터 `AlreadyExists` 로 막힌다.
|
|
|
|
**문제가 생기면** — 적용 자체가 거절되면 클러스터가 서 있는지부터 본다. 앞 단계의 `kubectl get nodes` 가 두 줄을 내는지가 전제다. `error: the path ... does not exist` 면 저장소 루트가 아닌 곳에서 쳤다.
|
|
|
|
### 2. 두 파드가 다 설 때까지 기다린다
|
|
|
|
**목적** — Keycloak StatefulSet 의 파드 둘이 Ready 가 될 때까지 다음 명령을 치지 않는다.
|
|
|
|
```bash label="[lab host] ① 끝날 때까지 멈춰 있는 것이 정상이다"
|
|
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s
|
|
```
|
|
|
|
**예상 결과**
|
|
|
|
```text
|
|
partitioned roll out complete: 2 new pods have been updated...
|
|
```
|
|
|
|
이 명령은 끝날 때까지 아무것도 안 찍고 멈춰 있다. 그 침묵이 정상이고, 마지막 한 줄에서 `complete` 라는 낱말과 파드 개수 `2` 를 본다. StatefulSet 은 파드를 하나씩 순서대로 띄우므로 중간에 `0/2` 로 한참 멈춰 있는 것도 정상이다.
|
|
|
|
**왜 필요한가** — 300초를 다 쓰고 타임아웃으로 끝나면 그것도 답이 된다. 「안 떴다」가 확정되고 진단으로 넘어간다. `rollout status` 를 쓰는 까닭은 `get pods` 를 반복해서 치는 것보다 나아서만이 아니라, 언제 끝났는지를 사람이 판정하지 않아도 되기 때문이다. 기다리지 않고 판정으로 가면 아직 안 뜬 것과 못 뜨는 것이 섞인다.
|
|
|
|
**문제가 생기면** — 타임아웃으로 끝났으면 아래 「막히면」의 순서대로 본다.
|
|
|
|
## 구성 값
|
|
|
|
만들어지는 객체는 「이 단계가 세우는 것」의 표에 있고, 여기서는 그 값들이 어디서 왔는지를 적는다.
|
|
|
|
| 값 | 어디서 정해지나 |
|
|
|---|---|
|
|
| 네임스페이스 이름 | 매니페스트 첫 문서 |
|
|
| 파드 이름의 순번 | StatefulSet 이라 `keycloak-0` · `keycloak-1` |
|
|
| 파드 이름의 해시 | Deployment 가 만든 ReplicaSet 이름에서 물려받는다 |
|
|
| Secret 의 바이트 수 | 매니페스트가 넣은 값의 길이. 이 문서는 값을 적지 않는다 |
|
|
| PVC 가 붙는 노드 | `local-path` 가 파드 스케줄을 기다렸다 그 노드에 만든다 |
|
|
| Ingress 의 HOSTS | 앞 단계에서 발급한 인증서의 이름과 같아야 한다 |
|
|
|
|
Ingress 의 호스트 이름이 인증서의 이름과 다르면, 밖에서는 TLS 는 되는데 `404` 가 나온다.
|
|
|
|
## 끝났는지 판정한다
|
|
|
|
`kubectl get pods` 만 보면 놓치는 것이 많다. 위에서 아래로 확인한다.
|
|
|
|
### 확인 ① 무엇이 만들어졌나
|
|
|
|
**무엇을 확인하는가** — 이 네임스페이스에 무엇이 서 있는지.
|
|
|
|
```bash label="[lab host] ① 워크로드와 Service 를 본다"
|
|
kubectl -n keycloak-lab get all
|
|
```
|
|
|
|
```bash label="[lab host] ② 앞 명령이 안 세는 넷을 따로 본다"
|
|
kubectl -n keycloak-lab get secret,configmap,pvc,ingress
|
|
```
|
|
|
|
**어디를 봐야 하는가** — ① 은 종류별로 묶여 나오는 왼쪽 이름 열을 훑고, 파드 줄에서는 READY 칸의 `1/1` 과 RESTARTS 칸을 본다. RESTARTS 가 0 이 아니면 지금은 `Running` 이어도 한 번 죽었다 살아난 것이다. ② 는 네 종류가 하나씩이라도 있는가, PVC 줄의 STATUS 가 `Bound` 인가, Ingress 줄의 HOSTS 칸이 `auth.hyeonworks.com` 인가.
|
|
|
|
**이 결과가 의미하는 것** — `get all` 은 이름과 달리 전부가 아니다. Secret 과 ConfigMap 과 PVC 와 Ingress 가 ① 의 출력에 나오지 않으므로, 첫 명령만 보고 「다 만들어졌다」로 판정하면 빠진 것을 모른 채 다음으로 간다. 매니페스트에 있는데 ② 에 없는 종류가 있다면 `apply` 가 부분적으로만 먹었다.
|
|
|
|
### 확인 ② Deployment 에서 ReplicaSet 을 지나 파드까지 이어졌나
|
|
|
|
**무엇을 확인하는가** — 사슬 어디까지 갔는지. Deployment 는 파드를 직접 만들지 않고 ReplicaSet 을 만들며 그것이 파드를 만든다.
|
|
|
|
```bash label="[lab host] 해시로 사슬을 맞춰 본다"
|
|
kubectl -n keycloak-lab get rs,pod -l app=postgres
|
|
```
|
|
|
|
**실측**(observed) — 2026-09-11
|
|
|
|
```text
|
|
NAME DESIRED CURRENT READY AGE
|
|
replicaset.apps/postgres-7b474b88c8 1 1 1 80m
|
|
|
|
NAME READY STATUS RESTARTS AGE
|
|
pod/postgres-7b474b88c8-ll87p 1/1 Running 0 80m
|
|
```
|
|
|
|
**어디를 봐야 하는가** — ReplicaSet 이름의 해시가 파드 이름 가운데 해시와 같은가, 그리고 `DESIRED` 와 `CURRENT` 와 `READY` 세 숫자가 다 `1` 인가.
|
|
|
|
**이 결과가 의미하는 것** — 이 출력에 Deployment 줄이 없는 것도 정상이다. 이 매니페스트는 `app: postgres` 라벨을 파드 템플릿에만 달았고, ReplicaSet 과 파드는 템플릿에서 라벨을 물려받지만 Deployment 는 물려받지 않는다. Deployment 를 보려면 라벨 없이 친다.
|
|
|
|
```bash label="[lab host] Deployment 는 라벨 없이 본다"
|
|
kubectl -n keycloak-lab get deploy
|
|
```
|
|
|
|
StatefulSet 쪽은 사슬이 한 마디 짧다. ReplicaSet 을 만들지 않고 파드를 직접 만들어서 `keycloak-0` 처럼 순번 이름이 붙는데, 해시를 끼워 넣을 중간 객체가 없기 때문이다. 이름이 고정이라 같은 이름의 파드가 둘일 수 없고, 그래서 `Terminating` 파드가 안 지워지면 대체 파드도 안 생긴다. 가이드는 그 성질이 어느 실험에서 어떻게 나타나는지까지 적어 두었는데 그 실험 문서는 여기 반입되지 않았고, 이 실험대가 그 상태를 재현해 본 적도 없다.
|
|
|
|
```bash label="[lab host] Keycloak 쪽에는 ReplicaSet 줄이 없다"
|
|
kubectl -n keycloak-lab get sts,rs,pod -l app=keycloak
|
|
```
|
|
|
|
```text
|
|
NAME READY STATUS RESTARTS AGE
|
|
pod/keycloak-0 1/1 Running 0 19m
|
|
pod/keycloak-1 1/1 Running 0 19m
|
|
```
|
|
|
|
배포를 여러 번 한 Deployment 는 ReplicaSet 이 여러 개 쌓인다. 새로 만들고 옛것은 `0` 으로 남기기 때문이고, 그래서 `kubectl rollout undo` 가 가능하다. 파드가 옛 해시를 달고 있으면 새 배포가 아직 안 넘어온 것이고, 그 상태로 실험하면 고친 적 없는 코드를 재게 된다.
|
|
|
|
| 무엇이 보이나 | 어디를 보나 |
|
|
|---|---|
|
|
| Deployment 는 있는데 RS 가 없다 | 컨트롤러가 못 돌았다 — RBAC·admission 확인 |
|
|
| RS 는 있는데 DESIRED 만 있고 CURRENT 가 0 | 파드를 못 만든다 — 이벤트를 본다 |
|
|
| Pod 은 있는데 `0/1` | 컨테이너가 안 뜬다 — 로그와 describe |
|
|
|
|
### 확인 ③ Secret 이 파드까지 이어졌나
|
|
|
|
**무엇을 확인하는가** — 값이 있는 것과 파드가 그 값을 받은 것은 다르다. 세 확인 모두 값을 찍지 않고 길이만 본다.
|
|
|
|
```bash label="[lab host] ① Secret 에 무슨 키가 얼마만큼 들어 있나"
|
|
kubectl -n keycloak-lab describe secret keycloak-lab-secrets
|
|
```
|
|
|
|
**실측**(observed) — 아래쪽 `Data` 절만 옮겼다
|
|
|
|
```text
|
|
Data
|
|
====
|
|
KC_BOOTSTRAP_ADMIN_PASSWORD: 19 bytes
|
|
POSTGRES_PASSWORD: 22 bytes
|
|
```
|
|
|
|
```bash label="[lab host] ② 키 하나가 의심스러울 때만 다시 잰다"
|
|
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
|
-o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d | wc -c
|
|
```
|
|
|
|
```bash label="[lab host] ③ 파드 안에 주입됐나 — 여기가 진짜다"
|
|
kubectl -n keycloak-lab exec keycloak-0 -- \
|
|
sh -c 'echo "길이=${#KC_BOOTSTRAP_ADMIN_PASSWORD}"'
|
|
```
|
|
|
|
```text
|
|
길이=19
|
|
```
|
|
|
|
**어디를 봐야 하는가** — ① 은 `Data` 절의 키 이름과 그 옆의 바이트 수 두 칸, ② 와 ③ 은 숫자 하나다. 세 수가 서로 같은가.
|
|
|
|
**이 결과가 의미하는 것** — `describe` 는 값을 절대 찍지 않고 길이만 보여 주므로 키 목록 확인과 「비어 있지 않은가」 확인이 한 명령으로 끝난다. 세 수가 같으면 Secret 에서 파드 환경변수까지 이어졌다. `길이=0` 이면 Secret 에는 있는데 이 파드가 그것을 안 받은 것이라, `envFrom` 이나 `valueFrom` 을 빠뜨렸거나 파드가 Secret 을 고치기 전에 떠서 옛 값을 들고 있다. 환경변수로 주입한 Secret 은 값을 바꿔도 파드를 다시 만들기 전까지 갱신되지 않는다. 바이트 수가 뜻밖에 크면, 이를테면 20 이어야 할 것이 21 이면 만들면서 개행이 같이 들어간 것이고 증상은 「비밀번호가 틀렸다」로 나온다. 매니페스트가 기대하는 키 이름이 하나라도 다르면 파드는 `CreateContainerConfigError` 로 멈추고, 까닭은 `describe pod` 의 Events 에 키 이름까지 적혀 나온다.
|
|
|
|
`-o yaml` 로 보지 않는다. base64 는 암호화가 아니라 인코딩이라 화면과 스크롤백과 화면 공유와 터미널 로그에 값이 그대로 찍힌다.
|
|
|
|
어느 환경변수가 어느 Secret 에서 왔는지도 볼 수 있다.
|
|
|
|
```bash label="[lab host] Secret 에서 온 변수만 오른쪽에 이름이 붙는다"
|
|
kubectl -n keycloak-lab get pod keycloak-0 \
|
|
-o jsonpath='{range .spec.containers[0].env[*]}{.name}{"\t"}{.valueFrom.secretKeyRef.name}{"\n"}{end}'
|
|
```
|
|
|
|
```text
|
|
KC_DB
|
|
KC_DB_URL
|
|
KC_DB_USERNAME
|
|
KC_DB_PASSWORD keycloak-lab-secrets
|
|
```
|
|
|
|
오른쪽 칸이 채워진 줄만 Secret 을 참조한다. 비밀이어야 할 변수의 오른쪽이 비어 있으면 그 값은 매니페스트에 평문으로 적혀 있다는 뜻이고, 그 파일은 대개 git 에 들어간다.
|
|
|
|
### 확인 ④ Service 뒤에 파드가 있나
|
|
|
|
**무엇을 확인하는가** — Service 가 있어도 셀렉터가 안 맞으면 뒤가 비어 있다.
|
|
|
|
```bash label="[lab host] Endpoints 를 센다"
|
|
kubectl -n keycloak-lab describe svc keycloak | grep -i endpoints
|
|
```
|
|
|
|
```text
|
|
Endpoints: 10.42.0.67:8080,10.42.1.155:8080
|
|
```
|
|
|
|
**어디를 봐야 하는가** — 쉼표로 갈린 주소가 몇 개인가, 그 IP 들이 파드 IP 와 같은가, 포트 번호가 컨테이너가 실제로 듣는 포트인가.
|
|
|
|
**이 결과가 의미하는 것** — 두 개면 Service 가 두 파드를 다 잡고 있다. 비어 있으면 Service 는 있는데 뒤가 없는 것이고, 이때 증상이 「연결은 되는데 응답이 없다」라 원인이 Service 에 있다는 것을 알아보기 어렵다. 하나뿐이면 나머지 한 파드가 readiness 를 통과하지 못한 것이라, 그 상태로 이중화 실험을 시작하면 이미 한쪽으로만 가고 있던 트래픽을 이중화 실패로 오독하게 된다.
|
|
|
|
목록으로 보려면 EndpointSlice 를 쓴다.
|
|
|
|
```bash label="[lab host] 같은 것을 EndpointSlice 로도 본다"
|
|
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak
|
|
```
|
|
|
|
```text
|
|
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
|
|
keycloak-xdph6 IPv4 8080 10.42.0.67,10.42.1.155 3d23h
|
|
```
|
|
|
|
`kubectl get endpoints` 는 쓰지 않는다. v1.33 부터 deprecated 이고 실행하면 경고가 나온다(external) — 이 실험대의 k3s 는 `v1.36.4+k3s1` 이라 해당한다. 옛 문서와 블로그에 그 형태가 많다.
|
|
|
|
준비 상태까지 함께 보려면 이렇게 뽑는다.
|
|
|
|
```bash label="[lab host] 주소마다 ready 가 true 인지 본다"
|
|
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
|
|
-o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{"\t"}{.conditions.ready}{"\n"}{end}'
|
|
```
|
|
|
|
```text
|
|
10.42.0.67 true
|
|
10.42.1.155 true
|
|
```
|
|
|
|
`ready` 가 `false` 면 파드는 있는데 readiness 프로브를 통과하지 못한 것이라 Service 가 그 파드로 트래픽을 보내지 않는다. 비어 있으면 셀렉터와 파드 라벨이 안 맞는다.
|
|
|
|
```bash label="[lab host] 셀렉터와 파드 라벨을 나란히 본다"
|
|
kubectl -n keycloak-lab get svc keycloak -o jsonpath='{.spec.selector}'; echo
|
|
kubectl -n keycloak-lab get pods --show-labels
|
|
```
|
|
|
|
### 확인 ⑤ PVC 가 실제로 붙었나
|
|
|
|
**무엇을 확인하는가** — 볼륨이 실제로 잡혔는지.
|
|
|
|
```bash label="[lab host] PVC 의 상태와 StorageClass 를 본다"
|
|
kubectl -n keycloak-lab get pvc
|
|
```
|
|
|
|
**어디를 봐야 하는가** — STATUS 칸이 `Bound` 인가 `Pending` 인가, VOLUME 칸이 비어 있는지, STORAGECLASS 칸이 `local-path` 인가. StatefulSet 이면 PVC 이름 끝에 파드 번호가 붙어 있어 어느 파드 것인지 바로 보인다.
|
|
|
|
**이 결과가 의미하는 것** — `Bound` 면 볼륨이 붙었다. `Pending` 이면 StorageClass 가 없거나 노드에 자리가 없다. `local-path` 는 파드가 스케줄될 때까지 기다리므로, 파드가 안 뜨면 PVC 도 `Pending` 인 것이 정상이다. 둘이 서로를 기다리는 것처럼 보이지만 파드 쪽 원인을 먼저 본다. 사유는 PVC 의 이벤트에 적혀 있다.
|
|
|
|
```bash label="[lab host] 이름을 안 주면 전부 나온다"
|
|
kubectl -n keycloak-lab describe pvc
|
|
```
|
|
|
|
각 PVC 절의 맨 아래 `Events` 에 `waiting for first consumer` 인지 `no persistent volumes available` 인지가 적혀 있고, 둘은 대응이 다르다 — 앞엣것은 파드를 고치는 문제, 뒤엣것은 저장소를 고치는 문제다.
|
|
|
|
### 확인 ⑥ 클러스터가 묶였나 — 셋이 서로 다른 것을 말한다
|
|
|
|
**무엇을 확인하는가** — 두 Keycloak 이 하나의 클러스터로 묶였는지. 셋이 각각 다른 시점을 말한다. 로그는 「그때 그렇게 보였다」이고, 테이블은 「지금 등록되어 있다」이며, 지표는 「지금 그 노드가 그렇게 안다」이다.
|
|
|
|
```bash label="[lab host] 로그 — 그때 본 클러스터 뷰"
|
|
kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
|
|
```
|
|
|
|
```text
|
|
ISPN000094: Received new cluster view for channel ISPN:
|
|
[keycloak-0-10001|1] (2) [keycloak-0-10001, keycloak-1-52537]
|
|
```
|
|
|
|
**어디를 봐야 하는가** — 세 군데다. 괄호 안의 `(2)` 가 멤버 수, 대괄호 안의 이름 목록, 그리고 `|1` 이 뷰 번호다. 뷰 번호는 멤버가 들고 날 때마다 올라간다.
|
|
|
|
이름 목록의 `keycloak-0-10001` 은 파드 이름 그대로가 아니다. Infinispan 이 파드 이름 뒤에 임의의 접미사를 붙여 클러스터 노드 식별자로 쓰기 때문에, 아래 지표에 나오는 `keycloak-0-46674` 와는 `keycloak-0` 까지만 같다. 그래서 대조할 때 맞춰 보는 것은 접미사 앞의 파드 이름이다. Keycloak 만 StatefulSet 이라 그 앞부분이 고정이고, 덕분에 로그와 `jgroups_ping` 테이블과 지표를 같은 이름으로 견줄 수 있다.
|
|
|
|
**이 결과가 의미하는 것** — `(2)` 면 이 노드는 상대를 봤다. `(1)` 이면 혼자 있다고 알고 있다. 뷰 번호가 계속 오르고 있으면 멤버가 붙었다 떨어지기를 반복하는 중이라, 「지금 2 다」라는 스냅숏보다 그 사실이 더 중요하다. 이 줄은 과거형이므로 지금 상태는 지표로 본다.
|
|
|
|
```bash label="[lab host] 테이블 — 지금 등록되어 있는 멤버"
|
|
kubectl -n keycloak-lab exec deploy/postgres -- \
|
|
psql -U keycloak -d keycloak -c 'select name, ip from jgroups_ping'
|
|
```
|
|
|
|
행이 몇 개인가, 그리고 `ip` 칸이 확인 ④ 에서 본 파드 IP 와 같은가를 본다. 이 표는 「등록되어 있다」이지 「서로 말이 통한다」가 아니다. 두 행이 다 있는데 로그가 `(1)` 이면 서로를 찾기는 했는데 7800 포트로 메시지가 안 가는 것이고, 이 실험대에서 실제로 그 일이 벌어졌다(observed). 옛 파드의 행이 남아 있을 수도 있으므로 IP 를 지금 파드와 대조한다.
|
|
|
|
```bash label="[lab host] 지표 — 지금 각 노드가 아는 멤버 수"
|
|
kubectl -n observability exec deploy/prometheus -- \
|
|
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
|
|
```
|
|
|
|
**실측**(observed) — 줄바꿈 없는 JSON 한 줄에서 읽어낸 값이다
|
|
|
|
```text
|
|
keycloak-1 → 2
|
|
keycloak-0 → 2
|
|
```
|
|
|
|
응답은 줄바꿈 없는 JSON 한 덩어리로 온다. `data.result` 배열에서 원소가 몇 개인가, 각 원소에서 `metric` 안의 `node` 라벨과 `value` 배열의 둘째 원소만 읽는다. 이 실험대에는 `jq` 가 없으므로 파서를 따로 짜지 말고 화면에 나온 JSON 을 그대로 읽는다. 원소가 하나뿐이면 나머지 노드의 스크레이프가 실패한 것이라 클러스터 문제가 아니라 관측 문제일 수 있다. 각 노드가 자기가 아는 멤버 수를 보고하므로 한 노드만 보면 분단을 놓친다 — 분단되면 한쪽은 2, 다른 쪽은 1 이 된다.
|
|
|
|
:::warning
|
|
|
|
Keycloak 컨테이너에는 `curl` 이 없다. 공식 이미지가 최소 구성이라 `wget` 도 `nc` 도 없고, 안에서 치면 `command not found` 와 exit code 127 로 끝난다.
|
|
|
|
:::
|
|
|
|
```text
|
|
sh: line 1: curl: command not found
|
|
command terminated with exit code 127
|
|
```
|
|
|
|
Prometheus 가 아직 없으면 임시 파드를 띄운다.
|
|
|
|
```bash label="[lab host] 파드 IP 를 꺼내 임시 파드에서 긁는다"
|
|
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'"
|
|
```
|
|
|
|
```text
|
|
vendor_cluster_size{cache_manager="keycloak",node="keycloak-0-46674"} 2.0
|
|
```
|
|
|
|
줄 끝의 숫자와 중괄호 안 `node` 라벨이 어느 파드인가를 본다. 이 형태는 그 파드 하나에게 직접 물은 것이라 라벨의 노드 이름과 고른 파드가 반드시 일치한다. `--rm` 을 붙였으므로 파드는 끝나면 사라진다. 값이 안 나오고 연결 거부가 나면 관리 포트 9000 이 안 열렸다.
|
|
|
|
### 확인 ⑦ 밖에서 닿나
|
|
|
|
**무엇을 확인하는가** — nginx 에서 Traefik, Ingress, Service 를 지나 파드까지 전부 이어졌는지.
|
|
|
|
```bash label="[lab host] 앞 두 단계와 같은 명령"
|
|
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
|
```
|
|
|
|
```text
|
|
200
|
|
```
|
|
|
|
**어디를 봐야 하는가** — 코드 한 칸. 여기서 값만 뽑는 형태를 쓰는 것은 앞 두 단계에서 잰 것과 같은 명령으로 같은 값이 나오는지 비교하는 것이 목적이기 때문이다.
|
|
|
|
**이 결과가 의미하는 것** — `200` 이면 2홉이 다 이어졌다. `502` 나 `503` 이면 뒤에서부터 되짚는다 — Ingress 가 있는지(확인 ①), Service 뒤에 파드가 있는지(확인 ④), 파드가 Ready 인지(확인 ②) 순서다. 처음 보는 오류라 헤더가 필요하면 읽는 형태로 바꾼다.
|
|
|
|
```bash label="[lab host] 처음 보는 오류는 헤더까지 읽는다"
|
|
curl -I https://auth.hyeonworks.com/realms/master
|
|
```
|
|
|
|
### 확인 ⑧ 관리 콘솔에 로그인된다
|
|
|
|
**무엇을 확인하는가** — 가이드가 적은 「이 단계가 끝나면」의 앞 절반.
|
|
|
|
브라우저로 `https://auth.hyeonworks.com/admin` 에 들어가 관리자로 로그인한다. 비밀번호는 확인 ③ 의 Secret 에 있고, 이 문서는 그 값을 적지 않는다 — 이 실험대의 확인은 전부 길이까지만 본다.
|
|
|
|
세션이 실제로 어디 저장되는지까지 보려면 DB 를 직접 본다. 평소에는 필요 없다.
|
|
|
|
```bash label="[lab host] 로그인 전과 후에 두 번 재서 차이를 본다"
|
|
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` 를 로그인 전과 후에 두 번 재서 그 차이를 본다. 한 번만 재면 아무것도 알 수 없다. 로그인 뒤 수가 늘면 세션이 DB 에 남는 것이고, 안 늘면 메모리에만 있다. 메모리에만 있으면 파드를 재시작하는 순간 세션이 사라지고, DB 에 있으면 살아남는다.
|
|
|
|
## 통과 조건을 한 번에 다시 본다
|
|
|
|
| 무엇 | 명령 | 통과 |
|
|
|---|---|---|
|
|
| 롤아웃 | `kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=300s` | `complete` · 파드 `2` |
|
|
| Service 뒤 | `kubectl -n keycloak-lab describe svc keycloak \| grep -i endpoints` | 주소 두 개 |
|
|
| 클러스터 | `… query=vendor_cluster_size` | 원소 둘 · 값 둘 다 2 |
|
|
| 밖에서 | `curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master` | `200` |
|
|
|
|
## 막히면
|
|
|
|
가이드 05 가 순서를 정해 두었다. 로그부터 보지 않는다.
|
|
|
|
**① 이벤트부터.** 스케줄링과 이미지와 볼륨 실패가 여기 나온다.
|
|
|
|
```bash label="[lab host] 최근 이벤트를 시간순으로 본다"
|
|
kubectl -n keycloak-lab get events --sort-by=.lastTimestamp | tail -20
|
|
```
|
|
|
|
맨 아랫줄부터 거꾸로 읽는다. `--sort-by` 를 준 까닭이 그것이다. TYPE 이 `Warning` 인 줄, REASON 칸, 그리고 OBJECT 칸이 어느 파드인가를 본다. 이벤트는 기본 한 시간만 남으므로 아무것도 없다고 문제가 없는 것은 아니다. REASON 하나가 다음 행동을 정한다 — `FailedScheduling` 은 노드에 자리가 없는 것이라 파드가 아니라 클러스터를 봐야 하고, `ErrImagePull` 은 이미지 이름 문제라 로그를 볼 것도 없으며, `BackOff` 는 컨테이너가 떴다가 죽는 중이라 ③ 으로 간다.
|
|
|
|
**② describe.** 그 파드에 한정된 이벤트와 상태를 함께 본다.
|
|
|
|
```bash label="[lab host] 파드 하나를 자세히 본다"
|
|
kubectl -n keycloak-lab describe pod keycloak-0
|
|
```
|
|
|
|
위에서부터 세 곳이다. `Conditions` 절에서 `Ready` 가 `False` 인가, 컨테이너 절의 `State` 와 `Last State` 와 그 안의 `Exit Code`, 그리고 맨 아래 `Events`. Exit Code 는 그것만으로 말이 된다 — `137` 은 OOM 이나 강제 종료, `1` 은 애플리케이션이 스스로 끝낸 것, `127` 은 명령을 못 찾았다는 뜻이다. `137` 이면 로그에 아무 단서도 없을 수 있어 메모리 한도를 본다. `Ready` 만 `False` 이고 컨테이너는 살아 있으면 readiness 프로브 문제이므로 확인 ④ 로 돌아간다.
|
|
|
|
**③ 로그.** 컨테이너가 떴는데 죽는 경우를 본다.
|
|
|
|
```bash label="[lab host] 지금 로그와 재시작 직전 로그"
|
|
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 label="[lab host] 컨테이너 안의 셸로 들어간다"
|
|
kubectl -n keycloak-lab exec -it keycloak-0 -- sh
|
|
```
|
|
|
|
| 증상 | 어디를 보나 |
|
|
|---|---|
|
|
| 파드가 `Pending` | `describe pod` 의 Events — 스케줄 불가 사유 |
|
|
| `ImagePullBackOff` | 이미지 이름과 태그. 자체 빌드면 두 노드 모두에 반입했는가 |
|
|
| `CrashLoopBackOff` | `logs --previous` |
|
|
| `Running` 인데 `0/1` | readiness 프로브 실패. `describe` 의 Conditions |
|
|
| 밖에서 502 | Ingress 에서 Service 로, Service 에서 Endpoints 로 뒤를 본다 |
|
|
| 클러스터가 1로 보임 | 7800 이 막혔거나 디스커버리 실패. 확인 ⑥ 의 셋 다 |
|
|
| `AlreadyExists` | `kubectl create namespace` 를 먼저 쳤다. 매니페스트가 만든다 |
|
|
| `error: the path ... does not exist` | 저장소 루트가 아닌 곳에서 쳤다 |
|
|
|
|
## 무엇이 관측이고 무엇이 아닌가
|
|
|
|
- (observed) 2026-09-11 의 ReplicaSet 과 파드 이름과 해시, Secret 의 19 와 22 bytes 와 주입된 길이 19 와 복호 길이 22, Endpoints 두 개와 EndpointSlice 의 `ready` 두 줄, `ISPN000094` 뷰 줄, `vendor_cluster_size` 가 둘 다 2, 임시 파드가 읽은 `2.0`, Keycloak 이미지에 `curl` 이 없다는 것과 exit code 127.
|
|
- (observed) 디스커버리 테이블에는 둘 다 있는데 메시지가 안 가는 상태를 실제로 겪었다.
|
|
- (external) `kubectl get endpoints` 의 v1.33 deprecation 은 쿠버네티스 쪽 규격이고 이 실험대가 잰 값이 아니다.
|
|
- (external) Infinispan 이 파드 이름에 임의의 접미사를 붙여 클러스터 노드 식별자로 쓴다는 것은 Infinispan 의 동작이다. 이 기록에 실린 `keycloak-0-10001` 과 `keycloak-0-46674` 가 그렇게 생긴 이름이다(observed).
|
|
- (unknown) `keycloak-cluster.yaml` 원문이 반입되지 않아 Keycloak 과 PostgreSQL 의 이미지 태그, 파드 자원 한도, 프로브 설정, `persistent-user-sessions` 설정값은 대조하지 못했다. 위에 적은 객체 이름과 해시와 바이트 수는 돌고 있던 실험대에서 읽은 것이고, 매니페스트가 그것들을 어떤 값으로 선언했는지는 여기서 확인할 수 없다.
|
|
- (unknown) `get all` 과 `get deploy` 와 `get pvc` 와 `describe pvc` 와 디스커버리 테이블 조회와 `get events` 와 `describe pod` 와 `logs` 의 출력은 가이드가 봐야 할 줄만 적었고 값이 남아 있지 않다.
|
|
- (unknown) 원본 가이드 05 에 되돌리는 절차가 없다. `local-path` PVC 가 노드 디스크에 남긴 데이터가 네임스페이스를 지울 때 어떻게 되는지도 확인하지 않았다.
|
|
- (unknown) 이 단계에는 「이 실험대는 이렇게 했다」와 「따라 하는 사람은」 두 갈래가 없다. 05 는 파일을 직접 쓰지 않고 저장소의 매니페스트를 적용한다.
|
|
- (unknown) 만드는 명령은 구축할 때 친 것을 옮긴 것이라 다시 쳐서 같은 상태가 되는지는 확인되지 않았다.
|
|
|
|
<!-- body:end -->
|