test-server 를 비우고 다시 세운 뒤 Setup 기록 35편(virtualization 9 ·
keycloak-session-store 26)을 문서에 적힌 명령 그대로 쳤다. 어긋난 자리를
기록과 SSOT 양쪽에 실측과 함께 넣었다.
막히던 것
- 04 의 인증서 경로가 live/hyeonworks.com 이라 nginx 가 [emerg] 로 안 떴다.
실제 계보는 live/auth.hyeonworks.com 이고 「문제가 생기면」은 진단이 거꾸로였다
- 인증서가 와일드카드가 아니다. SAN 이 auth·app1·app2 셋뿐이라 그 밖의 이름은
TLS 에서 끊기고 curl 이 exit 60 · %{http_code} 000 을 낸다. SSOT 안에서
두 문단이 서로 어긋나 있었다
- A-7 14번 ①이 kc-lab-1 에서 여섯 줄 다 실패하는데 마지막 date 만 「차단」을 찍는다
검사가 실패할 수 없던 자리
- B-1 의 세션 키 고르기는 앞 단계가 $KEY 를 채워 둬서 루프가 한 건도 못 맞혀도
통과한다. KEY= 로 비우고 키마다 1/0 을 찍게 바꿨다
- k3s-agent 유닛의 sed -i 는 패턴에 $HOME 이 들어 있어 아무 줄도 안 바꾼 채 성공한다
certbot
- renew --dry-run 의 종료 코드는 성공도 0, 실패도 0, 다른 사유의 실패는 1 이다.
본문의 renew failure(s) 로만 판정할 수 있다
- --dry-run 은 staging 서버를 쓰는데 renewal/*.conf 의 account= 는 운영 계정을
가리킨다. 실패한 dry-run 이 staging 계정을 하나 더 만들어 다음 실행이 계속 멎는다
- 훅을 755 로 놓고 시뮬레이션이 성공해도 Running deploy-hook command 는 안 나온다.
certbot 2.1.0 에는 --run-deploy-hooks 도 없다
- 강제 갱신은 실제로 쳤고 서빙까지 닿았다. serial 06F3E0EF…1373 → 065547…3DF1,
notAfter Dec 3 → Dec 16, nginx worker 2629 4712 → 4745 4754
독자가 칠 수 있는 형태로
- 안 되는 형태가 번호 붙은 단계에 앉아 있던 8곳을 뒤집고, 되는 형태를 ①로 올렸다
- 랩 안에서 공개 이름을 치는 curl 65줄에 --resolve 를 붙였다. 붙인 형태를 실제로
쳐서 문서가 적은 값과 같은지 확인했다
- 힙독·sed -i·echo >>·&&·|| 를 편집기 + 파일 리스팅 + 분할 형태로 바꿨다
- 닫는 코드펜스가 빠져 뒤 200여 줄의 블록 종류가 뒤집혀 있던 곳을 포함해 3곳을 고쳤다
관문: check_body PASS · check_prose error 0 · check_evidence 두 프로젝트 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0 · 코드펜스 전수 0건
남은 것: B-0 주입은 keycloak-pattern 저장소의 소스를 고치고 이미지를 다시 구워야
해서 안 했다(unknown).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
849 lines
49 KiB
Markdown
849 lines
49 KiB
Markdown
---
|
||
id: af9645a0-8ca8-481d-9624-69fce6449b7c
|
||
kind: SETUP
|
||
slug: reproduce-b5-redis-loss
|
||
title: Redis 를 0대로 내리고 파드가 Ready 를 유지하는지 본다
|
||
topic: where-application-state-lives
|
||
topicName: 세션과 토큰을 Redis 와 PostgreSQL 에 나눠 두기
|
||
project: keycloak-session-store
|
||
status: 게시 전
|
||
studio: "https://hyeonworks.com/studio/documents/af9645a0-8ca8-481d-9624-69fce6449b7c/edit"
|
||
pinnedVersions:
|
||
- name: Redis
|
||
version: 7.4.x
|
||
- name: netty-transport
|
||
version: 4.1.135.Final
|
||
source:
|
||
- final/document.md#b층-재현-절차-아홉-편을-직접-치는-순서-b-5
|
||
sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||
---
|
||
|
||
# Redis 를 0대로 내리고 파드가 Ready 를 유지하는지 본다
|
||
|
||
Redis 를 0대로 내렸을 때 무엇이 멈추는지 재는 절차다. 세 경로와 health 그룹과 Service 엔드포인트를 보고, 이어서 볼륨을 뗀 채 파드를 지워 영속화 설정만으로 무엇이 남는지 본다. 세션은 돌아오지 않으므로 실험대에서만 하고, 되돌리려면 매니페스트를 다시 적용한다.
|
||
|
||
## 관계
|
||
|
||
- **볼륨 없는 영속화와 유예 없는 키 회전**
|
||
이 절차가 만드는 상태에서 나온 판정이다. 여기는 순서만 적고 결론은 그쪽이 적는다.
|
||
- **readiness 가 깨진 노드를 시야에서 먼저 치운다**
|
||
A-2 는 readiness 가 파드를 뺐고 여기는 안 뺀다. 무엇이 그 차이를 만드는지 다룬다.
|
||
- **up 지표는 살아 있지만 쓸모없는 상태를 보지 못한다**
|
||
파드가 `Ready` 인 채로 계속 실패하는 동안 지표가 무엇을 말하는지 다룬다.
|
||
- **PostgreSQL 을 정상 종료시키고 네 경로를 잰다**
|
||
같은 모양의 실험을 Keycloak 쪽에서 한 편이다. 두 결과를 견주는 것이 이 절차의 결론이다.
|
||
- **Redis 를 붙이고 무엇이 옮겨졌는지 빈 목록으로 견준다**
|
||
먼저 해 둬야 하는 편이다. 세션이 Redis 에 있어야 잃는 것이 보인다.
|
||
|
||
## 본문
|
||
|
||
<!-- body:start -->
|
||
|
||
## 읽기 전에 — 어디서 치는가
|
||
|
||
명령은 `[lab host]` 에서 `kubectl` 로 친다. `kubectl` 에 `sudo` 를 붙이지 않는다 — root 홈에는 kubeconfig 가 없어 `localhost:8080` 으로 붙으려다 끝난다. 브라우저는 시작 전에 한 번 쓴다. 세션이 Redis 에 하나는 있어야 잃는 것이 보인다.
|
||
|
||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||
|
||
| 무엇 | 값 |
|
||
|---|---|
|
||
| 네임스페이스 | `keycloak-lab` |
|
||
| 주입 수단 ① | `scale deployment/redis --replicas=0` — 없는 상태가 유지된다 |
|
||
| 주입 수단 ② | `volumeMounts` 와 `volumes` 를 patch 로 떼고 파드를 지운다 |
|
||
| Redis | `redis.keycloak-lab.svc:6379`, 파드는 `kc-lab-2` 에 고정 |
|
||
| 재는 경로 | 셋 — `/` · `/bff/token-boundary` · `/actuator/health` |
|
||
| 전 구간 | 약 30분 |
|
||
| 잃는 것 | 로그인 세션. 돌아오지 않는다 |
|
||
|
||
**이 실험대는 그 뒤로 바뀌었다.** 지금 매니페스트(`bff-redis.yaml`)에는 B-5 의 결론이 이미 반영되어 PVC 와 `--appendonly yes` 가 들어 있다. 그래서 둘째 주입은 볼륨 없는 상태를 다시 만드는 단계부터 시작한다. 원래 실행은 반대 순서였다 — 볼륨 없는 상태에서 시작해 PVC 를 붙였다(unknown).
|
||
|
||
## 이 실험이 가르는 것
|
||
|
||
A-2 에서 Keycloak 의 PostgreSQL 을 내렸을 때는 이렇게 됐다.
|
||
|
||
```text
|
||
DB 정지 → 헬스체크 실패 → 파드 NotReady → Service 에서 빠짐 → 밖에서 503
|
||
```
|
||
|
||
명확한 실패였다. `503` 은 지금 안 된다고 말하고, 클라이언트는 재시도든 포기든 정할 수 있다. 통념은 의존 저장소가 죽으면 헬스체크가 알아서 파드를 빼 준다는 것이고, 이 절차는 진짜 그런지와 이번에는 무엇을 보고 판단하는지를 잰다.
|
||
|
||
두 번째 질문이 붙는다.
|
||
|
||
```text
|
||
Redis 를 다시 띄우면 → 세션이 남아 있나?
|
||
```
|
||
|
||
영속화를 켜 두면 된다는 통념이 쿠버네티스에서 어떻게 어긋나는지를 잰다. 그래서 영속화를 논하기 전에 `/data` 가 무엇인지부터 보는 절이 이 절차에서 가장 무겁다.
|
||
|
||
## 전제와 되돌리기
|
||
|
||
- `05-keycloak` · `06-observability` 가 끝나 있다.
|
||
- B-1 · B-2 가 끝나 세션은 Redis, 토큰은 PostgreSQL 로 나뉘어 있다. 나뉘어 있어야 각각 죽여볼 수 있고, 이 절차는 Redis 만 죽인다.
|
||
- 브라우저로 `https://app1.hyeonworks.com/` 에 로그인해 둔다(`labuser` / `labpass`).
|
||
|
||
**저장소를 지우는 실험이다.** Redis 를 0대로 내리고 나중에 볼륨 없이 파드를 지운다. 그 안의 세션은 돌아오지 않고 로그인한 사용자는 전부 로그아웃된다. 중간에 그만두려면 한 줄이면 된다.
|
||
|
||
```bash label="[lab host] 중간에 그만둘 때 치는 한 줄"
|
||
kubectl -n keycloak-lab scale deployment/redis --replicas=1
|
||
```
|
||
|
||
볼륨을 뗀 뒤에는 매니페스트를 다시 적용해 되돌린다. 그 두 줄이 둘째 주입의 유일한 되돌리기다. `deploy/lab/k8s/bff-redis.yaml` 은 저장소 안의 상대 경로다 — 저장소를 체크아웃한 디렉터리에서 쳐야 풀리고, 다른 디렉터리에서 치면 경로가 없다는 오류로 끝나 볼륨이 안 돌아온다. 그 체크아웃이 `kc-lab-1` 의 어디에 있는지는 원본 가이드에 없다(unknown).
|
||
|
||
```bash label="[lab host] 볼륨을 뗀 뒤에 되돌리는 두 줄"
|
||
kubectl apply -f deploy/lab/k8s/bff-redis.yaml
|
||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||
```
|
||
|
||
## 주입 전에 같은 명령으로 먼저 본다
|
||
|
||
```text
|
||
파드 → Redis 내용 · 영속화 설정 → ★ /data 가 볼륨인가 → 세 경로 → health 그룹
|
||
```
|
||
|
||
### 1. 파드가 어디에 몇 개 있는가
|
||
|
||
**무엇을 보는가** — 파드 넷의 상태와 배치.
|
||
|
||
```bash label="[lab host] 파드 배치를 본다"
|
||
kubectl -n keycloak-lab get pods -o wide
|
||
```
|
||
|
||
**어디를 보나** — 모양은 이렇다(observed).
|
||
|
||
```text
|
||
NAME READY STATUS RESTARTS AGE IP NODE
|
||
bff-555df79c97-6j86w 1/1 Running 0 17m 10.42.0.52 kc-lab-1
|
||
bff-555df79c97-vgg6g 1/1 Running 0 16m 10.42.1.124 kc-lab-2
|
||
postgres-... 1/1 Running 0 5d ... kc-lab-2
|
||
redis-... 1/1 Running 0 3d ... kc-lab-2
|
||
```
|
||
|
||
**이 값이 뜻하는 것** — `bff` 가 둘 다 `1/1` 이고 `RESTARTS` 가 `0` 이다. Redis 는 하나라 replica 가 없고, 0으로 내리면 전면 정지다. Redis 와 PostgreSQL 이 같은 노드(`kc-lab-2`)인 것은 매니페스트가 `nodeSelector` 로 고정한 결과이고, A-4(노드 상실)에서 두 저장소가 한꺼번에 없어지게 하려는 배치다. `10.42.0.52` 와 `10.42.1.124` 는 실제 BFF 파드 IP 이고, 관찰 절에서 이 두 주소가 다시 나온다.
|
||
|
||
### 2. Redis 안에 무엇이 있고 영속화가 어떻게 설정돼 있는가
|
||
|
||
**무엇을 보는가** — 키 수와 두 영속화 설정.
|
||
|
||
```bash label="[lab host] Redis 의 내용과 영속화 설정을 본다"
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli ping
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli config get save
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli config get appendonly
|
||
```
|
||
|
||
**어디를 보나** — 실측은 이렇다(observed, `01-baseline.txt`).
|
||
|
||
```text
|
||
=== 기준선 ===
|
||
Redis 키: 1
|
||
PostgreSQL 토큰: 1 행
|
||
Redis 영속화 설정:
|
||
save = save
|
||
appendonly no
|
||
```
|
||
|
||
| 값 | 그때 | 뜻 |
|
||
|---|---|---|
|
||
| 키 수 | `1` | 로그인 세션 하나 |
|
||
| `save` | 빈 값 | RDB 스냅샷이 꺼져 있다 |
|
||
| `appendonly` | `no` | AOF(append-only file, 쓰기를 순서대로 적어 두는 파일)도 꺼져 있다 |
|
||
|
||
**이 값이 뜻하는 것** — 그때는 영속화가 아예 꺼져 있었다. 지금 환경은 아마 다르다 — 매니페스트가 `--appendonly yes` 로 시작하므로 `appendonly yes` 가 나오고, 그 차이가 둘째 주입의 출발 조건이다.
|
||
|
||
**★ 2026-09-17 에 쳤더니 `save` 도 비어 있지 않았다**(observed). 이 이미지의 기본 스냅샷 조건 셋이 그대로 들어 있다. 그래서 둘째 주입의 출발 조건은 「AOF 만 켠 상태」가 아니라 「AOF 와 스냅샷이 둘 다 켜진 상태」이고, 그래도 결과는 같다 — 볼륨이 없으면 둘 다 컨테이너와 함께 사라진다.
|
||
|
||
```text
|
||
save
|
||
3600 1 300 100 60 10000
|
||
appendonly
|
||
yes
|
||
```
|
||
|
||
같은 날 `redis-cli --scan` 은 아무것도 안 찍었다. 브라우저 로그인을 안 한 실험대라 세션 키가 0개였고, 그 상태에서는 「잃는 것」이 안 보인다. 로그인을 먼저 해 두라는 전제는 이 때문이다.
|
||
|
||
`save` 출력의 값이 비어 있는 것과 그 설정 자체가 없는 것은 다르다. `config get save` 는 항상 두 줄(이름·값)을 돌려주고, 값 줄이 비어 있으면 스냅샷 조건이 없다는 뜻이다. 증거의 `save = save` 는 그 두 줄이 한 줄로 붙어 찍힌 모양이다.
|
||
|
||
### 3. `/data` 가 볼륨인가
|
||
|
||
**무엇을 보는가** — 영속화를 말하기 전에 확인할 셋. 이 확인을 건너뛰면 「AOF 를 켰는데 안 남는다」를 「Redis 가 이상하다」로 읽게 된다.
|
||
|
||
```bash label="[lab host] ① 파드에 볼륨이 붙어 있는가"
|
||
kubectl -n keycloak-lab get pod -l app=redis \
|
||
-o jsonpath='{.items[0].spec.volumes}'; echo
|
||
```
|
||
|
||
지금 매니페스트 기준의 모양은 이렇다(observed).
|
||
|
||
```json
|
||
[{"name":"data","persistentVolumeClaim":{"claimName":"redis-data"}}]
|
||
```
|
||
|
||
```bash label="[lab host] ② 어디에 붙었는지와 PVC 상태를 본다"
|
||
kubectl -n keycloak-lab get pod -l app=redis \
|
||
-o jsonpath='{.items[0].spec.containers[0].volumeMounts}'; echo
|
||
kubectl -n keycloak-lab get pvc
|
||
```
|
||
|
||
```text
|
||
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
|
||
redis-data Bound pvc-... 1Gi RWO local-path 3d
|
||
```
|
||
|
||
**어디를 보나** — 셋이 전부 성립해야 한다.
|
||
|
||
```text
|
||
① volumes 에 항목이 있다 ← 없으면 컨테이너 파일시스템이다
|
||
② volumeMounts 의 mountPath 가 /data ← 다른 데 붙었으면 소용없다
|
||
③ PVC 가 Bound ← Pending 이면 파드가 안 뜬다
|
||
```
|
||
|
||
**이 값이 뜻하는 것** — 하나라도 빠지면 `appendonly yes` 는 장식이다. 파일은 만들어지고 로그도 정상인데 재시작하면 사라진다.
|
||
|
||
```text
|
||
/data 가 볼륨이 아니다 → 이미지 위의 쓰기 가능 레이어에 쓴다
|
||
→ 컨테이너가 없어지면 그 레이어도 없어진다
|
||
```
|
||
|
||
Redis 는 이것을 모른다. `appendonly yes` 를 켜면 성실히 `/data` 에 `appendonlydir` 을 만들고 매 쓰기를 기록한다. 거짓말이 아니라 정말로 기록하고, 다만 그 디렉터리가 어디 있는지를 모른다. `emptyDir` 도 마찬가지다 — 컨테이너 재시작은 견디지만 파드가 없어지면 같이 없어진다. 볼륨을 붙였다와 영속 볼륨을 붙였다는 다르다.
|
||
|
||
### 4. 세 경로를 정상 상태에서 한 번 돌린다
|
||
|
||
**무엇을 보는가** — 주입 후에 볼 세 경로를 주입 전에 똑같은 명령으로.
|
||
|
||
```bash label="[lab host] 세 경로의 상태 코드를 뽑는다"
|
||
for p in / /bff/token-boundary /actuator/health; do
|
||
curl -s -o /dev/null -w "$p %{http_code} total %{time_total}\n" --max-time 90 \
|
||
--resolve app1.hyeonworks.com:443:192.168.122.10 "https://app1.hyeonworks.com$p"
|
||
done
|
||
```
|
||
|
||
**어디를 보나** — 증거에는 첫 줄만 남았다(observed, `01-baseline.txt`).
|
||
|
||
```text
|
||
=== 외부 진입점 정상 확인 ===
|
||
https://app1.hyeonworks.com/ HTTP 200
|
||
```
|
||
|
||
`000` 이 아닌 것이 판정 기준의 전부다.
|
||
|
||
| 경로 | 정상일 때 | 왜 |
|
||
|---|---|---|
|
||
| `/` | `200` | `permitAll` 정적 페이지. Redis 를 안 탄다 |
|
||
| `/bff/token-boundary` | `200` 또는 로그인으로 보내는 `3xx` | 세션이 필요하다 — Redis 를 탄다 |
|
||
| `/actuator/health` | `200` | 모든 지표의 합 |
|
||
|
||
**이 값이 뜻하는 것** — 셸의 `curl` 에는 로그인 쿠키가 없으므로 두 번째는 보통 `3xx` 이고, 가이드가 이 대목을 미검증으로 표시했다(unknown). `200` 이든 `3xx` 든 상관없다 — 이 절차가 보는 것은 응답이 오는가이고, `3xx` 를 만드는 과정에서도 BFF 는 세션을 만들려고 Redis 를 건드린다.
|
||
|
||
**`--max-time` 을 반드시 붙인다.** 주입 뒤 이 요청은 응답이 안 온다. 타임아웃이 없으면 터미널이 붙잡힌 채로 있고, 그 상태를 멈춤이 아니라 내 터미널이 이상함으로 읽게 된다.
|
||
|
||
### 5. health 그룹 셋이 서로 다른지 본다
|
||
|
||
**무엇을 보는가** — 세 응답의 본문. `/actuator/**` 는 이 실험대에서 열려 있다(운영에서는 절대 안 연다).
|
||
|
||
```bash label="[lab host] ① health 그룹 셋의 본문을 받는다"
|
||
curl -s --resolve app1.hyeonworks.com:443:192.168.122.10 \
|
||
https://app1.hyeonworks.com/actuator/health; echo
|
||
curl -s --resolve app1.hyeonworks.com:443:192.168.122.10 \
|
||
https://app1.hyeonworks.com/actuator/health/readiness; echo
|
||
curl -s --resolve app1.hyeonworks.com:443:192.168.122.10 \
|
||
https://app1.hyeonworks.com/actuator/health/liveness; echo
|
||
```
|
||
|
||
**어디를 보나** — 첫 번째 응답의 본문에 `redis` 항목이 있는지, 두 번째 응답에는 없는지를 본다. 세 응답이 서로 다르다는 것을 보는 것이 이 확인의 전부다.
|
||
|
||
정지 후 값이 증거에 이렇게 남아 있고, 그 본문 항목 칸은 비어 있다(observed, `03-health-groups.txt`).
|
||
|
||
```text
|
||
=== /actuator/health 본문 (Redis 항목이 있는가) ===
|
||
|
||
|
||
=== /actuator/health/readiness 본문 ===
|
||
{"status":"UP"}
|
||
```
|
||
|
||
**첫 칸이 비어 있는 것은 측정 실패다.** 파드 안에서 본문을 받아오려다 못 받았다. 밖에서 직접 재 두는 편이 낫다 — 뒤에서 이 값을 비교하게 된다.
|
||
|
||
```text
|
||
/actuator/health 모든 지표의 합 ← redis 지표가 여기 있다
|
||
/actuator/health/readiness readiness 그룹 ← 기본값은 readinessState 뿐
|
||
/actuator/health/liveness liveness 그룹
|
||
```
|
||
|
||
```bash label="[lab host] ② kubelet 이 보는 경로를 확인한다"
|
||
kubectl -n keycloak-lab get deploy bff \
|
||
-o jsonpath='{.spec.template.spec.containers[0].readinessProbe.httpGet.path}'; echo
|
||
```
|
||
|
||
**2026-09-17 에 Redis 를 내리기 전과 후를 같은 세 줄로 쟀다**(observed). 세 그룹이 어디서 갈리는지가 이 편의 전부다.
|
||
|
||
```text
|
||
Redis 살아 있을 때 Redis 0대일 때
|
||
/actuator/health UP DOWN ← redis: RedisConnectionFailureException
|
||
/actuator/health/readiness UP UP ← kubelet 이 보는 경로
|
||
/actuator/health/liveness UP UP
|
||
|
||
파드 1/1 Running 1/1 Running (둘 다)
|
||
Service 엔드포인트 ready true,true ready true,true
|
||
```
|
||
|
||
**Redis 가 통째로 사라졌는데 쿠버네티스는 아무것도 안 한다.** 합산 `health` 만 `DOWN` 이고 kubelet 이 보는 `readiness` 는 `UP` 이라 Service 가 두 파드로 트래픽을 계속 보낸다. `/actuator/health` 를 프로브로 걸었다면 두 파드가 동시에 빠져 전면 장애가 됐을 것이고, `readiness` 로 건 지금은 **아무 신호도 안 난다** — 어느 쪽이 맞는지가 아니라 **무엇을 고르면 무엇을 못 보게 되는지**가 이 세 줄에 있다.
|
||
|
||
```text
|
||
/actuator/health/readiness
|
||
```
|
||
|
||
**이 값이 뜻하는 것** — `redis` 헬스 지표는 자동으로 readiness 그룹에 들어가지 않는다. 그리고 kubelet 이 보는 것은 매니페스트가 지정한 경로다. 전체는 DOWN 인데 readiness 는 UP 인 상태가 성립한다.
|
||
|
||
## 주입
|
||
|
||
주입은 둘이다. 첫째는 Redis 를 0대로 내리고, 둘째는 볼륨을 뗀 채 영속화만 켜고 파드를 지운다. **둘째는 첫째를 되돌린 뒤에 한다.**
|
||
|
||
내리는 방법을 고른 이유부터 본다.
|
||
|
||
| 방법 | 만들어지는 상태 |
|
||
|---|---|
|
||
| `delete pod` | Deployment 가 **즉시 새로 만든다.** 몇 초짜리 공백이라 관찰할 시간이 없다 |
|
||
| **`scale --replicas=0`** | **없는 상태가 유지된다.** 내가 되돌릴 때까지 |
|
||
| NetworkPolicy 로 6379 차단 | 「연결 거부」와 「응답 없음」이 섞인다. A-1 에서 본 대로 기존 연결은 안 끊긴다 |
|
||
|
||
저장소가 없어진 상태를 안정적으로 유지하는 것이 목적이므로 두 번째를 쓴다. 파드가 사라지므로 주입 여부를 눈으로 확인하기도 쉽다.
|
||
|
||
### 1. Redis 를 0대로 내린다
|
||
|
||
**목적** — Redis 가 없는 상태를 만들고 그 상태를 유지한다.
|
||
|
||
① 시각을 남기고 replica 를 0으로 내린다.
|
||
|
||
```bash label="[lab host] 시각을 남기고 Redis 를 0대로 내린다"
|
||
date '+%H:%M:%S 정지'
|
||
kubectl -n keycloak-lab scale deployment/redis --replicas=0
|
||
```
|
||
|
||
**예상 결과** — 실측은 이렇다(observed, `02-redis-down.txt`).
|
||
|
||
```text
|
||
=== ① Redis 정지 ===
|
||
정지: 14:26:30
|
||
deployment.apps/redis scaled
|
||
삭제 완료
|
||
```
|
||
|
||
**왜 필요한가** — 시각을 반드시 적어 둔다. 언제부터 회복됐나를 붙일 때 쓴다.
|
||
|
||
**문제가 생기면** — 파드가 몇 초 만에 돌아왔다면 `delete pod` 를 쳤다. `scale --replicas=0` 으로 다시 한다.
|
||
|
||
### 2. 볼륨을 떼고 영속화만 켠다
|
||
|
||
**목적** — 영속화 설정은 켜져 있고 `/data` 는 컨테이너 파일시스템인 상태를 만든다. 첫째 주입을 되돌린 뒤에 한다.
|
||
|
||
**⓪ 먼저 Redis 를 다시 올린다.** 바로 앞 절에서 0대로 내려 두었고, 이 절의 명령은 전부 파드가 살아 있어야 한다. 올리지 않고 이어 치면 `exec deploy/redis` 가 붙을 파드를 못 찾는다.
|
||
|
||
```bash label="[lab host] ⓪ 앞 절의 주입을 되돌린다"
|
||
kubectl -n keycloak-lab scale deployment/redis --replicas=1
|
||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||
```
|
||
|
||
① `volumeMounts` 와 `volumes` 를 함께 뗀다. 가이드가 이 방향을 미검증으로 표시했다(unknown) — 원래 실행은 반대 순서였다.
|
||
|
||
```bash label="[lab host] ① 볼륨 참조를 떼고 롤아웃을 기다린다"
|
||
kubectl -n keycloak-lab patch deployment redis --type=json \
|
||
-p '[{"op":"remove","path":"/spec/template/spec/containers/0/volumeMounts"},
|
||
{"op":"remove","path":"/spec/template/spec/volumes"}]'
|
||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||
```
|
||
|
||
② AOF 를 켜고 키를 심은 뒤 `/data` 를 본다.
|
||
|
||
```bash label="[lab host] ② AOF 를 켜고 키를 심는다"
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli config set appendonly yes
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli config get appendonly
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli set b5:aof "written-with-aof"
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||
kubectl -n keycloak-lab exec deploy/redis -- ls -la /data
|
||
```
|
||
|
||
**예상 결과** — `appendonly yes` 가 나오고 `/data` 에 `appendonlydir` 이 생긴다.
|
||
|
||
**왜 필요한가** — **PVC 자체는 지우지 않는다.** Deployment 에서 참조만 뗐고, 나중에 `apply` 로 되돌리면 같은 PVC 에 다시 붙는다. PVC 를 지우면 `local-path` 프로비저너가 노드의 디렉터리까지 지운다.
|
||
|
||
**문제가 생기면** — 패치가 안 먹으면 `spec.volumes` 가 여전히 PVC 를 보여 준다. 주입 검증에서 그것부터 본다.
|
||
|
||
## 주입 검증
|
||
|
||
결과를 해석하기 전에, 주입이 의도한 것만 건드렸는지 본다.
|
||
|
||
**네 확인이 같은 시점을 보지 않는다.** ①②③ 은 첫째 주입이 걸려 있는 동안에만 성립한다 — 주입 1 절을 친 직후, 주입 2 절의 ⓪ 으로 Redis 를 다시 올리기 전에 본다. ④ 는 둘째 주입을 친 뒤라 Redis 가 1대로 살아 있을 때 본다. 절 순서대로 위에서 아래로 한 번에 치면 ① 이 `1/1` 을 내는데, 그것은 스케일이 안 먹은 증상이 아니라 ⓪ 이 제대로 올린 결과다.
|
||
|
||
```bash label="[lab host] ① Redis 가 0대인가"
|
||
kubectl -n keycloak-lab get pods -l app=redis
|
||
kubectl -n keycloak-lab get deploy redis
|
||
```
|
||
|
||
모양은 이렇다(observed).
|
||
|
||
```text
|
||
No resources found in keycloak-lab namespace.
|
||
|
||
NAME READY UP-TO-DATE AVAILABLE AGE
|
||
redis 0/0 0 0 3d
|
||
```
|
||
|
||
`0/0` 이어야 한다. `1/1` 이면 스케일이 안 먹었거나 다른 네임스페이스를 건드린 것이고, 그 상태에서 재는 것은 전부 무의미하다.
|
||
|
||
**응답이 없는 것과 붙지 못하는 것은 다르다.** 로그가 이유를 말한다.
|
||
|
||
```bash label="[lab host] ② BFF 로그에서 연결 시도를 찾는다"
|
||
kubectl -n keycloak-lab logs -l app=bff --tail=40 | grep -iE 'redis|connect|netty' | tail -10
|
||
```
|
||
|
||
실측은 이렇다(observed, `02-redis-down.txt`).
|
||
|
||
```text
|
||
=== BFF 로그 ===
|
||
at java.base/sun.nio.ch.Net.pollConnect(Native Method) ~[na:na]
|
||
at java.base/sun.nio.ch.Net.pollConnectNow(Unknown Source) ~[na:na]
|
||
at java.base/sun.nio.ch.SocketChannelImpl.finishConnect(Unknown Source) ~[na:na]
|
||
at io.netty.channel.socket.nio.NioSocketChannel.doFinishConnect(NioSocketChannel.java:336) ~[netty-transport-4.1.135.Final.jar!/:4.1.135.Final]
|
||
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.finishConnect(AbstractNioChannel.java:339) ~[netty-transport-4.1.135.Final.jar!/:4.1.135.Final]
|
||
```
|
||
|
||
**★ 이 실험대의 로그는 다른 줄을 냈다**(2026-09-17, observed). 스택이 아니라 Lettuce 의 재연결 로그다. Service 는 남아 있고 뒤에 파드가 없으므로 `Connection refused` 이고, 위의 「맺는 중」과 달리 여기서는 즉시 거절당한 뒤 다시 시도한다. 그래도 요청 쪽은 똑같이 매달린다.
|
||
|
||
```text
|
||
i.l.core.protocol.ConnectionWatchdog : Reconnecting, last destination was redis.keycloak-lab.svc/10.43.44.209:6379
|
||
i.l.core.protocol.ConnectionWatchdog : Cannot reconnect to [redis.keycloak-lab.svc/<unresolved>:6379]: Connection refused: redis.keycloak-lab.svc/10.43.44.209:6379
|
||
```
|
||
|
||
그리고 `grep -iE 'redis|connect|netty'` 는 `connect` 때문에 Hikari 의 PostgreSQL 커넥션 경고까지 잡는다. `tail -10` 의 앞쪽 여섯 줄이 그것이었고 Redis 와 무관하다.
|
||
|
||
`pollConnect` 와 `finishConnect` 는 연결을 맺는 중이라는 뜻이다. 이미 실패한 것이 아니라 아직 시도 중이고, Lettuce(Netty 기반 Redis 클라이언트)가 재연결을 시도하며 타임아웃을 기다린다. 관찰 절의 `000` 이 여기서 나온다.
|
||
|
||
```bash label="[lab host] ③ 엉뚱한 것을 죽이지 않았는지 본다"
|
||
kubectl -n keycloak-lab get pods
|
||
```
|
||
|
||
```text
|
||
=== 파드 상태 — readiness 가 Redis 를 보는가 ===
|
||
bff-555df79c97-6j86w 1/1 Running 0 17m
|
||
bff-555df79c97-vgg6g 1/1 Running 0 16m
|
||
```
|
||
|
||
`bff` 두 개의 `RESTARTS` 가 여전히 0 이고 postgres 가 살아 있어야 한다(observed). postgres 까지 내렸다면 B-5 가 아니라 전면 장애를 재게 된다. 여기서 이미 답이 절반 나와 있다 — Redis 가 없는데 `1/1` 이다.
|
||
|
||
**둘째 주입도 걸렸는지 본다.** 볼륨 확인은 주입 전과 똑같은 명령이다.
|
||
|
||
```bash label="[lab host] ④ 볼륨이 정말 떨어졌는가"
|
||
kubectl -n keycloak-lab get pod -l app=redis \
|
||
-o jsonpath='{.items[0].spec.volumes}'; echo
|
||
```
|
||
|
||
**빈 줄이 나와야 한다.** 여기서 여전히 PVC 가 보이면 패치가 안 먹은 것이고, 그 상태로 파드를 지우면 당연히 살아남는다 — 그리고 그걸 영속화가 잘 된다고 오독한다.
|
||
|
||
**★ 빈 줄은 안 나온다**(2026-09-17, observed). 쿠버네티스가 서비스 계정 토큰을 `kube-api-access-…` 라는 projected 볼륨으로 자동으로 붙이므로, `volumes` 배열을 통째로 지워도 새 파드에는 그 항목 하나가 다시 들어 있다. 길이가 아니라 이름을 본다 — `persistentVolumeClaim` 이 안 보이면 떨어졌다.
|
||
|
||
```text
|
||
[{"name":"kube-api-access-d7bbm","projected":{"defaultMode":420,"sources":[{"serviceAccountToken":{"expirationSeconds":3607,"path":"token"}},{"configMap":{"items":[{"key":"ca.crt","path":"ca.crt"}],"name":"kube-root-ca.crt"}},{"downwardAPI":{"items":[{"fieldRef":{"apiVersion":"v1","fieldPath":"metadata.namespace"},"path":"namespace"}]}}]}}]
|
||
```
|
||
|
||
`volumeMounts` 쪽으로 보면 더 짧다. `/data` 가 없고 `/var/run/secrets/kubernetes.io/serviceaccount` 하나만 남는다. 그리고 이 명령이 고르는 `.items[0]` 은 방금 지운 파드일 수 있다 — 같은 라벨에 `Completed` 파드가 남아 있는 것을 이 실험대에서 봤다.
|
||
|
||
**빈 줄에는 뜻이 둘이다.** 이 명령은 `app=redis` 라벨이 붙은 파드 중 첫째를 골라 그 파드의 `spec.volumes` 를 찍는다. Redis 가 0대면 고를 파드가 없어 아무것도 안 나오고, 그 화면은 볼륨을 뗐을 때와 구별되지 않는다. 그래서 이 줄을 읽기 전에 위 ① 의 `get pods -l app=redis` 로 Redis 파드가 하나 `Running` 인지부터 본다. 파드가 있는데 빈 줄이면 볼륨이 떨어졌고, 파드가 없으면 이 명령은 아직 아무 말도 하지 않았다.
|
||
|
||
```text
|
||
--- AOF 를 켜고 다시 심는다 (영속화가 켜져 있으면 살아남는가) ---
|
||
appendonly yes
|
||
total 12
|
||
drwxr-xr-x 3 redis redis 4096 Sep 4 05:26 .
|
||
drwxr-xr-x 1 root root 4096 Sep 4 05:26 ..
|
||
drwx------ 2 redis redis 4096 Sep 4 05:26 appendonlydir
|
||
```
|
||
|
||
`appendonlydir` 이 실제로 만들어졌다(observed, `04-persistence.txt`). Redis 는 시킨 대로 했다 — 설정도 `yes` 고 디렉터리도 있고 파일도 쓰인다. **여기서 영속화가 켜졌다고 결론 내리면 틀린다.** 어디에 쓰는지를 안 봤기 때문이고, 지금 `/data` 는 컨테이너 파일시스템이다.
|
||
|
||
## 관찰
|
||
|
||
**`000` 은 오류가 아니라 멈춤이다.** 주입 전과 똑같은 명령을 친다.
|
||
|
||
```bash label="[lab host] 세 경로를 다시 친다"
|
||
for p in / /bff/token-boundary /actuator/health; do
|
||
curl -s -o /dev/null -w "$p %{http_code} total %{time_total}\n" --max-time 90 \
|
||
--resolve app1.hyeonworks.com:443:192.168.122.10 "https://app1.hyeonworks.com$p"
|
||
done
|
||
```
|
||
|
||
실측은 이렇다(observed, `02-redis-down.txt`).
|
||
|
||
```text
|
||
=== 로그인한 사용자의 다음 요청은 어떻게 되는가 ===
|
||
/ HTTP 200
|
||
/bff/token-boundary HTTP 000
|
||
/actuator/health HTTP 503
|
||
```
|
||
|
||
| 코드 | 뜻 |
|
||
|---|---|
|
||
| `200` | 정적 페이지는 산다 — Redis 를 안 타는 경로 |
|
||
| **`000`** | **응답 자체를 못 받았다.** curl 이 기다리다 포기했다 |
|
||
| `503` | 헬스 엔드포인트는 대답은 한다 — 다만 DOWN 이라고 |
|
||
|
||
**★ 위 블록의 `--max-time 90` 은 원 가이드의 `--max-time 10` 을 고친 것이다**(2026-09-17, observed). 10초에서 끊으면 `/actuator/health` 도 `000` 이 되어 세 줄이 `200`·`000`·`000` 으로 나오고, 바로 위의 실측과 안 맞는다. 이 실험대에서 그 줄은 `60.046452` 초 뒤에 `503` 을 줬고 `/bff/token-boundary` 는 90초까지 기다려도 안 왔다. 그래서 90초까지 기다리게 하고 `%{time_total}` 로 걸린 시간을 같이 찍는다.
|
||
|
||
```text
|
||
health 503 total 60.046452
|
||
token-boundary 000 total 90.001609
|
||
```
|
||
|
||
그러니까 헬스 엔드포인트도 빨리 실패하지 않는다. 60초는 Redis 명령 타임아웃이고, 그때 돌아온 본문의 `redis` 항목은 「붙지 못했다」가 아니라 「명령이 시간을 넘겼다」였다.
|
||
|
||
```text
|
||
"redis":{"status":"DOWN","details":{"error":"org.springframework.dao.QueryTimeoutException: Redis command timed out"}}
|
||
```
|
||
|
||
오류를 돌려주는 것이 아니라 매달려 있다.
|
||
|
||
```text
|
||
빠른 실패: 요청 → 즉시 503 → 사용자는 오류 화면을 본다. 재시도할지 정할 수 있다
|
||
느린 실패: 요청 → ………… → 사용자는 멈춘 화면을 본다. 아무것도 정할 수 없다
|
||
```
|
||
|
||
빨리 실패하기(fail fast)가 안 되어 있다. A-6(지연 주입)에서 본 것과 같은 문제이고, 브라우저 탭도 그 앞의 로드밸런서도 그 앞의 사용자도 전부 붙잡힌다. 응답 본문도 비어 있다(observed, 같은 파일).
|
||
|
||
```text
|
||
--- token-boundary 응답 본문 ---
|
||
|
||
|
||
```
|
||
|
||
본문이 없다는 것은 오류 페이지조차 못 만들었다는 뜻이다. 고치려면 클라이언트에 타임아웃을 건다. Lettuce 의 연결·명령 타임아웃을 짧게 잡으면 `000` 이 `500` 이 되고, `500` 이 `000` 보다 낫다 — 적어도 말은 하기 때문이다.
|
||
|
||
**그런데 파드는 `Ready` 를 유지한다.** 이 절차의 가장 중요한 발견이다.
|
||
|
||
```bash label="[lab host] health 그룹 셋을 코드와 본문으로 본다"
|
||
curl -s -o /dev/null -w 'health %{http_code} total %{time_total}\n' --max-time 90 \
|
||
--resolve app1.hyeonworks.com:443:192.168.122.10 https://app1.hyeonworks.com/actuator/health
|
||
curl -s -o /dev/null -w 'readiness %{http_code}\n' --max-time 10 \
|
||
--resolve app1.hyeonworks.com:443:192.168.122.10 https://app1.hyeonworks.com/actuator/health/readiness
|
||
curl -s -o /dev/null -w 'liveness %{http_code}\n' --max-time 10 \
|
||
--resolve app1.hyeonworks.com:443:192.168.122.10 https://app1.hyeonworks.com/actuator/health/liveness
|
||
curl -s --resolve app1.hyeonworks.com:443:192.168.122.10 \
|
||
https://app1.hyeonworks.com/actuator/health/readiness; echo
|
||
```
|
||
|
||
실측은 이렇다(observed, `03-health-groups.txt`).
|
||
|
||
```text
|
||
=== health 그룹별 응답 — 왜 파드는 Ready 인가 ===
|
||
/actuator/health HTTP server
|
||
/actuator/health/readiness HTTP 200
|
||
/actuator/health/liveness HTTP 200
|
||
|
||
=== /actuator/health 본문 (Redis 항목이 있는가) ===
|
||
|
||
|
||
=== /actuator/health/readiness 본문 ===
|
||
{"status":"UP"}
|
||
```
|
||
|
||
`readiness` 가 `200` 이고 `{"status":"UP"}` 이다.
|
||
|
||
**첫 줄의 `HTTP server` 는 상태 코드가 아니라 측정이 실패한 것이다.** 값이 들어와야 할 칸에 엉뚱한 문자열이 들어와 있고, `503` 이라는 값은 `02-redis-down.txt` 쪽 측정에서 나왔다. 빈 값이나 이상한 값을 측정 결과로 읽지 않는다 — 그건 측정 실패다. A-1 에서도 빈 문자열을 변화로 읽어 판정이 틀어진 적이 있다. 이상하면 그 칸을 다시 친다.
|
||
|
||
```text
|
||
/actuator/health redis: DOWN → 전체 DOWN → 503
|
||
/actuator/health/readiness readinessState 만 → UP → kubelet: "정상"
|
||
```
|
||
|
||
그래서 Service 에서 파드를 빼지 않는다.
|
||
|
||
```bash label="[lab host] 엔드포인트가 아직 ready 인지 본다"
|
||
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=bff \
|
||
-o "custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready"
|
||
```
|
||
|
||
```text
|
||
=== Service 엔드포인트 — 트래픽을 계속 받는가 ===
|
||
ready: [10.42.0.52 10.42.1.124]
|
||
```
|
||
|
||
두 주소가 그대로 ready 다(observed). 주입 전에 본 그 두 IP 이고, 두 파드가 계속 트래픽을 받으며 계속 실패한다. 어느 replica 로 가도 결과가 같으므로 재시도해도 소용없다. `kubectl get endpoints` 는 쓰지 않는다 — v1.33 부터 deprecated 라 경고가 뜨고, 해설 문서 5절의 재현 절차에는 옛 형태(`get endpoints bff`)가 실려 있다.
|
||
|
||
A-2 와의 대비가 이 절차의 결론이다.
|
||
|
||
| | A-2 (Keycloak · DB 상실) | **B-5 (BFF · Redis 상실)** |
|
||
|---|---|---|
|
||
| 의존 대상 헬스 지표 | **readiness 에 포함** | **포함 안 됨** |
|
||
| 파드 상태 | **NotReady** | **Ready 유지** |
|
||
| Service 엔드포인트 | **비었다** | 둘 다 남는다 |
|
||
| 외부 응답 | **503** (즉시, 명확) | **000** (멈춤) |
|
||
|
||
Keycloak 은 자기 의존성을 readiness 에 넣었고 이 BFF 는 안 넣었다. 어느 쪽이 옳은지는 상황에 달렸다.
|
||
|
||
| readiness 에 넣으면 | 넣지 않으면 |
|
||
|---|---|
|
||
| 의존 대상이 죽으면 **전 파드가 빠진다** → 전면 장애 | 파드가 남아 **실패를 계속 서빙한다** |
|
||
| 부분 기능이라도 살릴 수 없다 | 부분 기능(정적 페이지 등)은 살아 있다 |
|
||
| A-2 처럼 **명확한 503** | **멈춤** — 진단이 어렵다 |
|
||
|
||
의도적으로 골라야 하는 설정이며, 기본값에 맡기면 후자가 된다. 넣기로 정했다면 명시한다.
|
||
|
||
```yaml
|
||
management:
|
||
endpoint:
|
||
health:
|
||
group:
|
||
readiness:
|
||
include: readinessState, redis # 넣으려면 명시해야 한다
|
||
```
|
||
|
||
liveness 에는 넣지 않는다. liveness 가 실패하면 kubelet 이 파드를 죽이는데, Redis 가 없어서 죽인 파드는 다시 떠도 Redis 가 없으므로 또 죽는다 — 재시작해도 안 나아지는 문제에 재시작을 걸게 된다.
|
||
|
||
**첫째 주입을 되돌리고 손대지 않는다.** BFF 를 재시작하고 싶은 충동을 참는다 — 재시작하면 스스로 회복하는가를 영영 알 수 없다.
|
||
|
||
```bash label="[lab host] ① Redis 를 다시 올린다"
|
||
date '+%H:%M:%S 복구'
|
||
kubectl -n keycloak-lab scale deployment/redis --replicas=1
|
||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||
```
|
||
|
||
①의 실측은 이렇다(observed, `04-persistence.txt`).
|
||
|
||
```text
|
||
=== 복구 ===
|
||
deployment.apps/redis scaled
|
||
deployment "redis" successfully rolled out
|
||
```
|
||
|
||
```bash label="[lab host] ② 회복했는지와 재시작 횟수를 본다"
|
||
for p in /actuator/health /bff/token-boundary; do
|
||
curl -s -o /dev/null -w "$p %{http_code} total %{time_total}\n" --max-time 90 \
|
||
--resolve app1.hyeonworks.com:443:192.168.122.10 "https://app1.hyeonworks.com$p"
|
||
done
|
||
kubectl -n keycloak-lab get pods -l app=bff
|
||
```
|
||
|
||
실측은 이렇다(observed, `04-persistence.txt`).
|
||
|
||
```text
|
||
/actuator/health HTTP 200
|
||
/bff/token-boundary HTTP 302
|
||
BFF 재시작 필요했나: 0,0 회 재시작
|
||
```
|
||
|
||
`재시작 0,0` 이다. Lettuce 가 스스로 재연결했다. A-2 에서 Keycloak 의 커넥션 풀이 그랬던 것과 같고, liveness 를 Redis 에 걸었다면 파드가 재시작됐을 것이며 회복이 더 늦어졌을 것이다. `302` 는 실패가 아니다 — 세션이 사라졌으므로 로그인으로 보내는 것이고, Redis 가 비었으니 사용자는 로그아웃된다. 여기서 다음 질문이 나온다 — Redis 를 다시 띄웠는데 왜 세션이 없나. 답은 영속화가 없었으니까다. 그럼 켜면 되나.
|
||
|
||
**둘째 주입의 결과가 그 답이다.** 볼륨 없이 AOF 만 켠 채 파드를 지운다.
|
||
|
||
`rollout status` 가 돌아와도 지운 파드가 아직 종료 중일 수 있다. 이어지는 `exec deploy/redis` 가 그 파드에 붙으면 명령이 실패하거나 지우기 전 숫자를 낸다. 이 절차의 판정이 바로 그 `dbsize` 이므로, 숫자가 이상하면 `get pods -l app=redis` 로 `Running` 하나만 남았는지 보고 다시 친다.
|
||
|
||
```bash label="[lab host] ③ 볼륨 없이 파드를 지우고 남은 것을 센다"
|
||
kubectl -n keycloak-lab delete pod -l app=redis
|
||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli get b5:aof
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli config get appendonly
|
||
```
|
||
|
||
```text
|
||
--- 파드를 지운다 ---
|
||
deployment "redis" successfully rolled out
|
||
재기동 후:
|
||
dbsize: 0
|
||
b5:probe
|
||
b5:aof
|
||
appendonly no
|
||
```
|
||
|
||
**★ 설정이 되돌아가는 것은 이 실험대에서 안 보인다**(2026-09-17, observed). 데이터는 그대로 사라져 `dbsize` 가 `0` 이고 `b5:aof` 는 빈 값인데, `config get appendonly` 는 `no` 가 아니라 `yes` 다. 지금 매니페스트의 `args` 가 이미 `--appendonly yes` 라서, 「재기동하면 매니페스트가 이긴다」는 규칙이 이번에는 켜진 값을 되살린다. 규칙은 그대로 맞다. 매니페스트가 이미 `yes` 라서 되돌아가도 값이 안 바뀔 뿐이다.
|
||
|
||
```text
|
||
0
|
||
|
||
appendonly
|
||
yes
|
||
```
|
||
|
||
볼륨을 되돌리고 같은 시험을 다시 했을 때는 `b5:pvc` 가 `written-on-pvc` 로 살아남았고 `dbsize` 는 `4` 였다. 나머지 셋은 그 사이에 BFF 가 만든 `bff:session:sessions:…` 키다.
|
||
|
||
두 가지가 같이 사라졌다(observed, 같은 파일).
|
||
|
||
| 사라진 것 | 왜 |
|
||
|---|---|
|
||
| **데이터** | `/data` 가 컨테이너 파일시스템이었다 — 컨테이너와 함께 없어졌다 |
|
||
| **설정** | `CONFIG SET` 은 **런타임 전용**이다. 재기동하면 매니페스트의 `args` 가 이긴다 |
|
||
|
||
쿠버네티스에서 영속화 설정만 켜는 것은 장식이다. `appendonly yes` 를 켜고 안심하는 것이 가장 위험하다 — 파일은 만들어지고 로그도 정상이며, 사라지는 것은 재시작 순간뿐이다. 그리고 재시작은 노드 정비·이미지 갱신·OOM(out of memory, 메모리가 모자라 커널이 프로세스를 죽이는 일) 어느 것으로든 일어난다. 설정이 되돌아간 것도 따로 중요하다. `CONFIG SET` 으로 고친 값은 `CONFIG REWRITE` 를 하지 않으면 파일에 안 남고, 컨테이너에서는 그 파일 자체가 안 남는다. 런타임 설정으로 영속 동작을 정하려는 시도는 두 겹으로 실패한다.
|
||
|
||
볼륨을 되돌리고 같은 시험을 다시 하면 결과가 갈린다.
|
||
|
||
```bash label="[lab host] ④ 볼륨을 되돌리고 같은 시험을 다시 한다"
|
||
kubectl apply -f deploy/lab/k8s/bff-redis.yaml
|
||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli set b5:pvc "written-on-pvc"
|
||
kubectl -n keycloak-lab delete pod -l app=redis
|
||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli get b5:pvc
|
||
```
|
||
|
||
```text
|
||
=== 영속 볼륨 위에서 다시 시험 ===
|
||
appendonly yes
|
||
키 심음: written-on-pvc
|
||
sed: -e expression #1, char 8: unknown option to 's'
|
||
|
||
--- 파드를 지운다 ---
|
||
deployment "redis" successfully rolled out
|
||
재기동 후:
|
||
dbsize: 1
|
||
b5:pvc written-on-pvc
|
||
```
|
||
|
||
`dbsize: 1` 과 `written-on-pvc` 로 살아남았다(observed, 같은 파일). 중간의 `sed: -e expression #1, char 8: unknown option to 's'` 는 원래 실행의 스크립트가 낸 오류이고 측정과는 무관하다. 값에 `/` 가 들어간 문자열을 `sed 's/.../.../'` 에 그대로 넣으면 이렇게 된다. 증거 파일에서 그 오류를 지우지 않은 것은 그것이 「이 줄은 스크립트가 만든 것」이라는 표시이기 때문이다.
|
||
|
||
| 구성 | 파드 삭제 후 |
|
||
|---|---|
|
||
| AOF **끔**, 볼륨 없음 | 전부 소실 |
|
||
| AOF **켬**, 볼륨 없음 | **전부 소실** (설정은 켰는데) |
|
||
| AOF **켬**, **PVC** | **생존** |
|
||
|
||
볼륨이 먼저고 설정이 나중이다. 순서를 바꾸면 두 번째 줄이 되고, 두 번째 줄은 첫 번째 줄과 결과가 같은데 안심하고 있다는 점에서 더 나쁘다.
|
||
|
||
`appendfsync` 는 그래도 맞바꿈이다. 기본값은 `appendfsync everysec` 이다.
|
||
|
||
| 설정 | 잃는 양 | 비용 |
|
||
|---|---|---|
|
||
| `always` | 없음 | 쓰기마다 fsync — 느리다 |
|
||
| **`everysec`** | **최대 1초** | 기본값 |
|
||
| `no` | OS 에 맡김 | 가장 빠름 |
|
||
|
||
세션 저장소에서 1초를 잃는다는 것은 그 사이 로그인한 사용자가 다시 로그인해야 한다는 뜻이다. A-3 에서 본 PostgreSQL 의 `synchronous_commit OFF` 와 같은 모양의 맞바꿈이고, 거기서 Keycloak 이 같은 판단을 했다.
|
||
|
||
PVC 도 노드에 못박힌다.
|
||
|
||
```bash label="[lab host] PVC 의 스토리지 클래스를 본다"
|
||
kubectl get pvc -n keycloak-lab redis-data -o jsonpath='{.spec.storageClassName}'; echo
|
||
```
|
||
|
||
```text
|
||
local-path
|
||
```
|
||
|
||
`local-path` 는 노드의 디렉터리다. A-4 에서 본 것과 같다 — 노드가 죽으면 볼륨도 함께 접근 불가가 되고 파드는 다른 노드로 못 옮겨간다. 영속화는 재시작을 견디게 하지만 노드 상실을 견디게 하지는 않는다.
|
||
|
||
**이 실험은 관측에 숙제를 남겼다.** Grafana 에 이 실험의 그래프가 없는데, 안 찍은 것이 아니라 지표가 없다.
|
||
|
||
```text
|
||
=== B층 구성 요소의 지표가 있는가 ===
|
||
redis_up 시계열 0개
|
||
redis_connected_clients 시계열 0개
|
||
pg_up 시계열 0개
|
||
pg_stat_database_numbackends 시계열 0개
|
||
```
|
||
|
||
Prometheus 가 긁는 대상에 Redis·PostgreSQL·BFF 가 애초에 없다(observed, `04-observability-gap.txt`). A층이 Grafana 증거를 남길 수 있었던 것은 Keycloak 이 `/metrics` 를 내놓고 그것을 scrape 대상에 넣어 뒀기 때문이다. 관측은 나중에 붙이는 것이 아니라 실험 설계에 포함되어야 한다 — Redis 가 언제 끊겼고 언제 붙었나를 초 단위로 보고 싶다면 `redis_exporter` 가 먼저 있어야 하고, 그건 실험이 끝난 뒤에는 못 만든다.
|
||
|
||
가이드는 무엇을 어떻게 붙일지까지 적어 두었다.
|
||
|
||
| 지표가 없는 대상 | 무엇을 붙이나 |
|
||
|---|---|
|
||
| Redis | `redis_exporter` 사이드카 또는 Deployment |
|
||
| PostgreSQL | `postgres_exporter` |
|
||
| BFF | 이미 actuator 가 있다 — `/actuator/prometheus` 노출 + scrape 추가 |
|
||
| 파드 readiness | `kube-state-metrics` (A-2 에서 이미 찾은 항목) |
|
||
|
||
## 복구와 원상복구 확인표
|
||
|
||
관찰 절이 이미 둘을 되돌렸다 — replica 를 1로 올렸고, `apply` 로 볼륨을 다시 붙였다. 이제 실험이 심은 키를 지우고 일곱 항목을 대조한다.
|
||
|
||
### 1. 매니페스트를 다시 적용해 볼륨과 설정을 되돌린다
|
||
|
||
**목적** — Deployment 를 저장소에 있는 모양으로 되돌린다.
|
||
|
||
① 관찰 절에서 이미 쳤더라도 한 번 더 친다. `apply` 는 같은 결과를 낸다.
|
||
|
||
```bash label="[lab host] 매니페스트를 다시 적용한다"
|
||
kubectl apply -f deploy/lab/k8s/bff-redis.yaml
|
||
kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||
```
|
||
|
||
**예상 결과** — `deployment "redis" successfully rolled out` 이 나온다.
|
||
|
||
**왜 필요한가** — patch 로 뗀 `volumeMounts` 와 `volumes` 가 여기서 돌아온다. `CONFIG SET` 으로 켠 `appendonly` 는 이미 재기동에서 매니페스트의 `args` 에 졌으므로 따로 되돌릴 것이 없다.
|
||
|
||
**문제가 생기면** — PVC 가 `Pending` 이면 `describe pvc redis-data` 의 Events 를 본다.
|
||
|
||
### 2. 실험이 심은 키를 지운다
|
||
|
||
**목적** — `b5:` 로 시작하는 키 셋만 치운다.
|
||
|
||
① 접두어로만 지운다. **`FLUSHALL` 은 치지 않는다** — BFF 세션과 oauth2-proxy 세션이 같은 Redis 에 있다.
|
||
|
||
```bash label="[lab host] 실험 키만 지우고 남은 키를 본다"
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli del b5:aof b5:pvc b5:probe
|
||
kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan
|
||
```
|
||
|
||
**예상 결과** — `--scan` 출력에 `b5:*` 가 없다.
|
||
|
||
**왜 필요한가** — `FLUSHALL` 을 치면 B-4 와 B-7 이 쓰는 `_oauth2_proxy-` 키까지 날아가고, 그 실험들이 뒤에 가서 갑자기 깨진다.
|
||
|
||
**문제가 생기면** — 지워지지 않으면 키 이름을 `--scan` 으로 먼저 눈으로 본다.
|
||
|
||
### 3. 일곱 항목을 대조한다
|
||
|
||
| 항목 | 명령 | 돌아왔을 때 |
|
||
|---|---|---|
|
||
| Redis | `kubectl -n keycloak-lab get deploy redis` | `1/1` |
|
||
| 볼륨 | `… get pod -l app=redis -o jsonpath='{.items[0].spec.volumes}'` | `persistentVolumeClaim` 이 보인다 |
|
||
| PVC | `kubectl -n keycloak-lab get pvc` | `redis-data` `Bound` |
|
||
| 영속화 | `… exec deploy/redis -- redis-cli config get appendonly` | `yes` |
|
||
| BFF | `kubectl -n keycloak-lab get pods -l app=bff` | 둘 다 `1/1`, `RESTARTS 0` |
|
||
| 엔드포인트 | `… get endpointslice -l kubernetes.io/service-name=bff` | ready 주소 **둘** |
|
||
| 실험 키 | `… redis-cli --scan` | `b5:*` 없음 |
|
||
| 밖 | `curl -s -o /dev/null -w '%{http_code}\n' --max-time 10 --resolve app1.hyeonworks.com:443:192.168.122.10 https://app1.hyeonworks.com/` | `200` |
|
||
|
||
**★ 이 편의 `curl` 에 `--resolve` 가 붙어 있는 까닭이다**(2026-09-17, observed). `app1.hyeonworks.com` 이 lab host 에서 `100.83.212.4` 로 풀리고 그 주소의 443 이 닫혀 있어, 이름만 치면 이 줄도 앞의 세 경로도 전부 `000` 이다. 그러면 주입 뒤와 견줄 값이 없다. 원 가이드가 적은 형태는 아래와 같고, 지우지 않고 남긴다.
|
||
|
||
```bash label="[lab host] 원 가이드가 적은 형태 — 이 실험대에서는 세 줄이 다 000 이다"
|
||
for p in / /bff/token-boundary /actuator/health; do
|
||
curl -s -o /dev/null -w "$p %{http_code}\n" --max-time 10 "https://app1.hyeonworks.com$p"
|
||
done
|
||
```
|
||
|
||
나머지 일곱 줄은 이 실험대에서 그대로 통과했다 — Redis `1/1`, 볼륨에 `persistentVolumeClaim`, PVC `redis-data` `Bound`, `appendonly yes`, BFF 둘 다 `1/1` 이고 `RESTARTS 0`, 엔드포인트 주소 둘, `--scan` 에 `b5:*` 없음.
|
||
|
||
**로그인 세션은 돌아오지 않는다.** 브라우저에서 다시 로그인하는 것이 복구다.
|
||
|
||
## 막히면
|
||
|
||
가이드는 이 표를 두고 전부 이 실험대가 실제로 겪은 증상이거나 그 기록에서 곧바로 따라 나오는 것이라고 적는다.
|
||
|
||
| 증상 | 원인 | 확인 |
|
||
|---|---|---|
|
||
| `curl` 이 안 끝나고 터미널이 붙잡힌다 | **그게 이 실험의 결과다.** `000` 이 되는 과정이다 | `--max-time` 을 붙인다 |
|
||
| `000` 을 서버 오류로 읽는다 | `000` 은 **응답을 못 받았다**는 curl 의 표기다 | `400`/`503` 과 구별한다 |
|
||
| **AOF 를 켰는데 안 남는다** | **`/data` 가 볼륨이 아니다** | 결론 내리기 전에 `spec.volumes` 를 먼저 |
|
||
| 파드를 지웠는데 데이터가 **살아남는다** | 볼륨 제거 패치가 안 먹었다 | `get pod … spec.volumes` 가 **비어야** 한다 |
|
||
| `config set` 한 값이 재기동 후 사라진다 | **런타임 전용이다.** 매니페스트 `args` 가 이긴다 | 재기동 후 `config get appendonly` |
|
||
| `/actuator/health` 응답 칸에 이상한 문자열 | **측정 실패다.** 값이 아니다 (`HTTP server`) | 그 칸을 다시 친다 |
|
||
| 파드가 `NotReady` 가 되기를 기다린다 | **안 된다.** `redis` 지표가 readiness 그룹에 없다 | health 그룹별 응답 |
|
||
| `kubectl get endpoints` 가 경고를 찍는다 | v1.33 부터 deprecated | `get endpointslice -l kubernetes.io/service-name=bff` |
|
||
| 회복 후 로그인이 풀려 있다 | **정상이다.** Redis 가 비었으니 세션이 없다 | `302` 는 실패가 아니다 |
|
||
| BFF 를 재시작해 버렸다 | 「스스로 회복하는가」를 못 재게 된다 | 주입부터 다시. 손대지 않고 기다린다 |
|
||
| PVC 가 `Pending` | `local-path` 프로비저너가 없거나 노드가 안 맞는다 | `describe pvc redis-data` 의 Events |
|
||
| 다른 실험이 갑자기 깨진다 | **`FLUSHALL` 을 쳤다.** 같은 Redis 를 나눠 쓴다 | 접두어로만 지운다 |
|
||
|
||
## 무엇이 관측이고 무엇이 아닌가
|
||
|
||
이 절차의 숫자는 `2026-09-04 14:24–14:28 KST` 에 돈 한 번의 실행에서 나왔다(observed).
|
||
|
||
- (observed) 주입 전 Redis 키 `1` 개 · PostgreSQL 토큰 `1` 행 · `save` 빈 값 · `appendonly no`, 정지 시각 `14:26:30`, 정지 후 세 경로의 `200` · `000` · `503` 과 빈 응답 본문, BFF 로그의 `pollConnect` · `finishConnect` 스택, 정지 중에도 `bff` 두 개가 `1/1` · `RESTARTS 0` 인 것, health 그룹의 `readiness 200` 과 `{"status":"UP"}`, 엔드포인트에 남은 `10.42.0.52` · `10.42.1.124`, 복구 후 `HTTP 200` · `HTTP 302` 와 `0,0 회 재시작`, `appendonlydir` 이 만들어진 `ls -la /data` 출력, 볼륨 없이 파드를 지운 뒤의 `dbsize: 0` · `appendonly no`, PVC 위에서 지운 뒤의 `dbsize: 1` · `written-on-pvc`, 후속 조사의 네 지표가 전부 시계열 0개인 것.
|
||
- **측정 실패를 값으로 읽지 않는다**(observed) — `03-health-groups.txt` 의 `/actuator/health HTTP server` 는 상태 코드가 아니고, 같은 파일의 `/actuator/health` 본문 칸도 비어 있다. `503` 은 `02-redis-down.txt` 쪽 측정에서 나온 값이다.
|
||
- (observed) `04-persistence.txt` 에 섞인 `sed: -e expression #1, char 8: unknown option to 's'` 는 원래 실행의 스크립트가 낸 오류이고 측정과 무관하다. 증거 파일에서 지우지 않았다.
|
||
- (unknown) 볼륨을 떼는 `patch deployment redis --type=json` 줄. **원래 실행은 반대 순서로 했다** — 볼륨 없는 상태에서 시작해 PVC 를 붙였고, 지금 실험대에서 같은 관찰을 하려면 이 방향이 된다. 셸 `curl` 에 로그인 쿠키가 없어 `/bff/token-boundary` 가 `3xx` 로 나오는 것도 가이드가 미검증으로 표시했다.
|
||
- 이 실험이 재지 않은 것 — Lettuce 타임아웃을 줄여 `000` 이 `500` 이 되는지는 재지 않았다. 「500 이 000 보다 낫다」까지가 이 실험의 결론이고 그 설정을 넣어 다시 잰 기록은 없다. `readiness` 그룹에 `redis` 를 넣었을 때 A-2 와 같은 모양이 되는지도 재지 않았다.
|
||
|
||
2026-09-17 에 이 절차를 1번부터 복구까지 다시 밟았다(observed). 판정은 전부 같았다 — Redis 를 0대로 내려도 `bff` 둘이 `1/1` 이고 엔드포인트 주소 둘이 그대로 `true,true` 였고, 볼륨 없이 파드를 지우면 `dbsize 0`, 볼륨을 되돌리면 `written-on-pvc` 가 살아남았고, 네 지표는 여전히 시계열 0개였다. 화면이 달랐던 다섯 자리는 위의 ★ 다섯이다. 원문은 `relive-2026-09-17/b5-04..07` 에 있다. 브라우저 로그인 전제는 이날 밟지 않아 세션 키가 0개인 채로 쟀다(unknown) — 「로그인한 사용자가 로그아웃된다」는 이 실행에서 확인하지 않았다.
|
||
|
||
<!-- body:end -->
|