Written by subagents running under the writing-practitioner-guides skill,
one guide per experiment, 22,566 lines. Each walks a reader from baseline
capture through injection, injection verification, observation and recovery.
Section 3 carries the weight in most of them. Injection failed silently nine
times in this lab, and a failed injection looks exactly like no effect — so
the guides verify the target is actually in the intended state before
reading any result. A-4 makes virsh list the only proof because the node
reads Ready for 40 seconds after the machine is off; A-5 makes the packet
counter the sole go/no-go because a rule on the wrong node produces an empty
result that reads like a finding; A-6 quotes the run where 적용완료 was
printed between four Cannot find device "eth0" lines.
The traps the guides are built around are ones that invert a conclusion
rather than merely annoy:
A-0 emptying the session table without a restart leaves cache entries
that get counted as replication arriving
A-2 dropping -o /dev/null fuses body and status into one string
A-3 presence of "ready to accept connections" instead of its timestamp
B-2 row count alone reads an UPDATE as nothing having happened
B-4 tr ',' '\n' splits ["admin","editor"] so only admin is seen
B-7 no login screen means the cookie died and SSO re-authenticated
C-1 counting sessions without joining realm counts your own kcadm one
D-1 kubectl exec without -i restores nothing and still exits 0
D-4a "ran with error output" is what success looks like
Every quoted block is copied from docs/evidence/ and marked 실측; reshaped
commands are marked 미검증 rather than passed off as measured. Where a source
document carries a ★ correction the guides follow the corrected claim — A-7's
REVOKED_TOKEN hypothesis, C-1's session count, B-2's schema attribution.
Two hazards are stated rather than smoothed over: B-6 deletes a key that
cannot be recreated, and D-1/D-4 need host sudo, which asks for a password,
so those steps say a person must type them.
Audit over all 26: 672 interpretation pairs, 486 evidence citations, 117
undo sections, and zero occurrences of the patterns the skill forbids —
no python data processing, no deprecated kubectl get endpoints, no
placeholders, no bare kcadm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
739 lines
30 KiB
Markdown
739 lines
30 KiB
Markdown
# D-2 재현 가이드 — 태그를 올리고 내려 보고 롤백이 되는 조건을 직접 본다
|
||
|
||
해설 문서: [`docs/experiment-d2-version-upgrade.md`](../../experiment-d2-version-upgrade.md) ·
|
||
증거 원문: [`docs/evidence/d2-version-upgrade/`](../../evidence/d2-version-upgrade/) ·
|
||
[`docs/evidence/followup/`](../../evidence/followup/)
|
||
|
||
## 이 가이드가 끝나면
|
||
|
||
당신 터미널에서 이것들을 **직접 본다.**
|
||
|
||
| 보게 되는 것 | 어디서 |
|
||
|---|---|
|
||
| 업그레이드 전후로 `databasechangelog` 행 수가 그대로인 것 | `psql -tAc` |
|
||
| 파드가 하나씩 갈리는 동안 정문이 계속 `200` 인 것 | 1초 폴링 |
|
||
| 같은 스키마에서는 **롤백이 되는 것** | 태그를 되돌리고 다시 폴링 |
|
||
| 전환 순간의 `000` 이 서버 오류가 **아닌** 것 | `--max-time` |
|
||
| 스키마가 바뀐 방향에서 `ValidationFailedException` 으로 기동이 거부되는 것 | `logs keycloak-1` |
|
||
| 그때도 서비스가 살아 있는 것 — StatefulSet 이 절반에서 멈춘다 | `endpointslice` |
|
||
| 실패한 기동이 스키마를 **안 건드린** 것 | 다시 `databasechangelog` |
|
||
|
||
## 전제
|
||
|
||
- [`D-1`](d1-backup-restore.md) 이 끝나 있고 **덤프가 손에 있다.** 이 실험의
|
||
되돌리기 수단은 태그가 아니라 그 파일일 수 있다.
|
||
- [`A-8`](../../experiment-a8-rolling-restart.md) — 롤링 재시작이 무중단이라는
|
||
것이 전제다.
|
||
- 명령은 **`kc-lab-1` 에서** 친다. `kubectl` 은 `sudo` 로 쓴다.
|
||
- 네임스페이스는 `keycloak-lab` 이다.
|
||
- `jq` 는 이 실험대 어디에도 없다. 이 가이드는 `jq` 를 쓰지 않는다.
|
||
- 터미널 **두 개**를 열어 두면 편하다. 하나는 가용성 폴링용, 하나는 관찰용.
|
||
|
||
## 주의 — 이건 실제로 버전을 바꾸는 실험이다
|
||
|
||
이미지 태그를 세 번 바꾼다(정방향 → 롤백 → 그리고 선택적으로 **실패하는**
|
||
방향). 마지막 것은 파드를 `CrashLoopBackOff` 로 만든다. **되돌리는 명령은
|
||
각 절 첫머리에 있고, 전부 태그 한 줄이다.** 전 구간 약 20분이며, 중간에
|
||
그만두려면 [6. 복구](#6-복구) 의 첫 명령 하나면 된다.
|
||
|
||
**그리고 이 실험은 백업 없이 시작하지 않는다.** 스키마가 움직이는 방향으로
|
||
가면 태그로는 못 돌아온다.
|
||
|
||
## 표시 규약
|
||
|
||
| 표시 | 뜻 |
|
||
|---|---|
|
||
| **실측** | 2026-09-04 15:00–15:26 KST 실행 기록의 **출력 원문**. 증거 파일에 그대로 있다 |
|
||
| **형태** | 값이 매번 달라지는 출력. 모양만 보이고 숫자는 당신 것과 다르다 |
|
||
| **미검증** | 손으로 치기 좋게 이 가이드에서 고친 형태. 원래 실행은 스크립트로 했다 |
|
||
|
||
**실측이 두 실행에서 나온다.** 처음 D-2 실행(15:00–15:10, 역방향 26.0)과
|
||
후속 실행(15:22–15:26, 26.7.3 정방향과 롤백)이다. 어느 쪽인지 매번 적는다.
|
||
|
||
---
|
||
|
||
# 0. 왜 이 실험을 하는가
|
||
|
||
「문제가 생기면 이미지 태그를 되돌린다」는 거의 모든 배포 계획서에 적혀 있다.
|
||
**그 계획이 언제 동작하고 언제 동작하지 않는가**를 가른다.
|
||
|
||
Keycloak 은 **Liquibase** 로 스키마를 관리한다. 적용한 변경 하나하나가
|
||
`databasechangelog` 테이블에 행으로 쌓이고, 각 행에는 그 변경 정의의
|
||
**체크섬(`md5sum`)**이 들어 있다.
|
||
|
||
```
|
||
컨테이너가 뜬다
|
||
└─▶ Liquibase 가 databasechangelog 를 읽는다
|
||
└─▶ 자기가 아는 changeset 의 체크섬과 대조한다
|
||
├─ 같다 → 기동
|
||
└─ 다르다 → ValidationFailedException. 기동 거부
|
||
```
|
||
|
||
**「모르는 변경이 있다」가 아니라 「아는 변경인데 정의가 다르다」이며, 더
|
||
엄격한 실패다.** 그래서 판정 기준은 이렇게 된다.
|
||
|
||
| 이렇게 묻지 말고 | 이렇게 묻는다 |
|
||
|---|---|
|
||
| 「26.7.3 에서 26.7.0 으로 내려도 되나?」 | **「`databasechangelog` 의 행 수가 바뀌었나?」** |
|
||
|
||
> **★ 이 가이드는 정정된 결론을 따른다.**
|
||
> 해설 문서는 처음에 「롤백은 안 된다」고 단정했다가 후속 실험에서 정정했다.
|
||
>
|
||
> | 버전 차 | `databasechangelog` | 롤백 |
|
||
> |---|---|---|
|
||
> | 26.7.0 → **26.0** | 체크섬 불일치 | **불가** |
|
||
> | 26.7.0 ↔ **26.7.3** | **210 → 210, 변화 없음** | **가능** |
|
||
>
|
||
> **판단 기준은 버전 번호가 아니라 행 수의 변화다.** 이 가이드는 그 숫자를
|
||
> 재는 법부터 가르친다.
|
||
|
||
---
|
||
|
||
# 1. 기준선 — 태그를 바꾸기 전에
|
||
|
||
**여기서 재 두지 않으면 나중에 다시 잴 수 없는 값이 하나 있다** —
|
||
업그레이드 **전**의 `databasechangelog` 행 수다. 올린 뒤에는 그 값이 지워지고,
|
||
「롤백해도 되는가」를 판정할 근거가 사라진다.
|
||
|
||
```
|
||
백업 → 현재 태그 → ★ 마이그레이션 수 → 세션 → 클러스터 뷰 → 가용성 대조군
|
||
```
|
||
|
||
## 1-1. 백업이 먼저다
|
||
|
||
**하기** — D-1 의 절차 그대로
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
|
||
--clean --if-exists > /tmp/pre-upgrade.sql
|
||
ls -l /tmp/pre-upgrade.sql
|
||
tail -3 /tmp/pre-upgrade.sql
|
||
```
|
||
**실측** — [`01-pre-upgrade.txt`](../../evidence/d2-version-upgrade/01-pre-upgrade.txt)
|
||
(첫 실행) · [`followup/01`](../../evidence/followup/01-d2-forward-upgrade.txt)(후속 실행)
|
||
```
|
||
백업: 396333 bytes
|
||
백업: 395375 bytes
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 크기, 그리고 `tail` 의 `dump complete`.
|
||
**이 파일이 없으면 이 실험을 하지 않는다.** 5절에서 왜인지 나온다.
|
||
|
||
## 1-2. 지금 무엇이 돌고 있나
|
||
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab get statefulset keycloak \
|
||
-o jsonpath='{.spec.template.spec.containers[0].image}'; echo
|
||
```
|
||
**실측** — [`01-pre-upgrade.txt`](../../evidence/d2-version-upgrade/01-pre-upgrade.txt)
|
||
```
|
||
quay.io/keycloak/keycloak:26.7.0
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 태그. **`latest` 로 되어 있으면 이 실험이 성립하지
|
||
않는다** — 무엇에서 무엇으로 가는지 말할 수 없기 때문이다.
|
||
|
||
StatefulSet 에 적힌 것과 **파드가 실제로 돌리고 있는 것**은 다를 수 있다
|
||
(적용 중이거나, 롤아웃이 멈춰 있으면).
|
||
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab get pods -o custom-columns=\
|
||
NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready,\
|
||
RESTARTS:.status.containerStatuses[0].restartCount | grep keycloak
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 두 파드의 IMAGE 가 **서로 같고** StatefulSet 과도
|
||
같은가, `READY` 가 둘 다 `true`, `RESTARTS` 가 `0`.
|
||
|
||
## 1-3. ★ 마이그레이션 수 — 이 숫자가 이 실험의 전부다
|
||
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "select count(*) from databasechangelog"
|
||
```
|
||
**형태**
|
||
```
|
||
count
|
||
-------
|
||
210
|
||
(1 row)
|
||
```
|
||
|
||
비교용으로 값만 뽑는 형태도 익혀 둔다.
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||
"select count(*) from databasechangelog"
|
||
```
|
||
**실측** — 두 실행 모두
|
||
```
|
||
총 마이그레이션 수: 210
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 숫자 하나. **이 값을 화면 밖에 적어 둔다.**
|
||
|
||
무엇이 마지막으로 적용됐는지도 한 번 본다. 나중에 「스키마가 언제 움직였나」를
|
||
물을 때 여기를 본다.
|
||
|
||
**확인** — **미검증**(원래 실행은 개수만 셌다)
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "select id, author, orderexecuted, dateexecuted from databasechangelog
|
||
order by orderexecuted desc limit 5"
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `dateexecuted` 의 가장 최근 값. **그게 이 DB 의
|
||
스키마가 마지막으로 움직인 시각이다.**
|
||
|
||
**이 결과가 의미하는 것** — 210 은 「이 DB 는 여기까지 올라갔다」는 기록이다.
|
||
업그레이드 후에 **211 이상이 되면 스키마가 움직인 것이고, 그 순간부터
|
||
태그만으로는 못 돌아온다.**
|
||
|
||
## 1-4. 세션 — 업그레이드가 로그인 상태를 날리는지 본다
|
||
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||
"select count(*) from offline_user_session"
|
||
```
|
||
**실측**
|
||
```
|
||
현재 세션: 4 (첫 실행)
|
||
세션 전: 3 (후속 실행)
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 숫자. 0 이면 관리 콘솔에 한 번 로그인해서 만든다.
|
||
**0인 채로 업그레이드하면 「세션이 유지되는가」를 판정할 수 없다.**
|
||
|
||
## 1-5. 클러스터 뷰 — Infinispan 판까지 적어 둔다
|
||
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
|
||
```
|
||
**실측** — [`followup/01`](../../evidence/followup/01-d2-forward-upgrade.txt) 의
|
||
**업그레이드 후** 값
|
||
```
|
||
cluster: [keycloak-1-11418(v=16.0.14)|47] (2) [keycloak-1-11418(v=16.0.14), keycloak-0-58996(v=16.0.14)]
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `(v=16.0.12)` 같은 괄호 안의 판, 그리고 멤버 수 `(2)`.
|
||
|
||
**이 결과가 의미하는 것** — Keycloak 태그를 바꾸면 **함께 실린 Infinispan 판도
|
||
같이 바뀐다.** 후속 실행에서 `16.0.12 → 16.0.14` 로 올라갔다. 클러스터 프로토콜
|
||
호환성 문제가 있다면 여기서 드러나므로, 업그레이드 후에 **이 줄이 멤버 2로
|
||
다시 서는지** 보는 것이 판정 항목 하나다.
|
||
|
||
## 1-6. 새 태그가 실제로 있는지 확인한다
|
||
|
||
**확인** — 레지스트리에 무엇이 있나. **처음 한 번은 그대로 본다**
|
||
```bash
|
||
curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyActiveTags=true"
|
||
```
|
||
|
||
한 줄짜리 JSON 이 통째로 나온다. 어떤 필드가 있는지 보고 나서 자른다.
|
||
|
||
**미검증** — `jq` 가 없으므로 이 실험대에서는 이렇게 읽는다
|
||
```bash
|
||
curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyActiveTags=true" \
|
||
| tr ',' '\n' | grep '"name"'
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `26.7.1` · `26.7.2` · `26.7.3` 이 있는가.
|
||
**처음 D-2 를 할 때 이걸 안 해서 정방향을 시험하지 못했다** — 「26.7.0 보다
|
||
새 이미지가 없다」고 적었지만 실제로는 셋이나 있었다.
|
||
|
||
## 1-7. 가용성 대조군 — 폴링을 먼저 띄운다
|
||
|
||
**주입 중에 나온 `000` 한 건을 해석하려면 평시 오류율을 알아야 한다.**
|
||
|
||
**하기** — 1초 간격으로 150회, 뒤에서 돌린다
|
||
```bash
|
||
( for i in $(seq 1 150); do
|
||
printf '%s ' "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 \
|
||
https://auth.hyeonworks.com/realms/master)"
|
||
sleep 1
|
||
done > /tmp/d2-avail.txt ) &
|
||
```
|
||
|
||
**되돌리기** — 그만 재려면
|
||
```bash
|
||
kill %1
|
||
```
|
||
|
||
**확인** — 30초쯤 두고 먼저 평시를 센다
|
||
```bash
|
||
tr ' ' '\n' < /tmp/d2-avail.txt | grep -c 200
|
||
tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `uniq -c` 의 **줄이 몇 개인가.** 한 줄이면 전부 같은
|
||
코드였다는 뜻이다. 두 줄 이상이면 **평시에 이미 오류가 있는 것**이고, 그
|
||
상태로 주입하면 주입 중의 오류를 귀속할 수 없다.
|
||
|
||
> **`--max-time 3` 을 기억해 둔다.** 4-4 에서 나오는 `000` 이 이 값 때문이다.
|
||
|
||
---
|
||
|
||
# 2. 주입 ① — 정방향 업그레이드 (26.7.0 → 26.7.3)
|
||
|
||
**되돌리기를 먼저 읽는다.**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab set image statefulset/keycloak \
|
||
keycloak=quay.io/keycloak/keycloak:26.7.0
|
||
```
|
||
|
||
**단, 이 되돌리기가 유효한 것은 `databasechangelog` 가 안 바뀌었을 때뿐이다.**
|
||
바뀌었으면 되돌리기는 「덤프 복구 + 태그 되돌리기」다(5절).
|
||
|
||
**하기**
|
||
```bash
|
||
date '+%H:%M:%S 태그 변경'
|
||
sudo kubectl -n keycloak-lab set image statefulset/keycloak \
|
||
keycloak=quay.io/keycloak/keycloak:26.7.3
|
||
```
|
||
**실측** — [`followup/01`](../../evidence/followup/01-d2-forward-upgrade.txt)
|
||
```
|
||
시작: 15:22:59
|
||
statefulset.apps/keycloak image updated
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `image updated` 한 줄. **이건 「적용됐다」가 아니라
|
||
「접수됐다」다.** 실제 교체는 지금부터 일어난다.
|
||
|
||
**하기** — 끝날 때까지 블록한다
|
||
```bash
|
||
sudo kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=600s
|
||
date '+%H:%M:%S 롤아웃 완료'
|
||
```
|
||
**실측**
|
||
```
|
||
partitioned roll out complete: 2 new pods have been updated...
|
||
완료: 15:24:26
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `2 new pods have been updated`. 87초 걸렸다.
|
||
|
||
> **`rollout status` 가 안 끝나고 매달려 있으면 그게 신호다.** StatefulSet 은
|
||
> 파드 하나가 Ready 가 되기 전에는 다음 파드를 안 건드린다. 즉 **매달림 =
|
||
> 첫 파드가 안 뜬다.** 다른 터미널에서 `get pods -w` 로 본다.
|
||
|
||
---
|
||
|
||
# 3. 주입이 실제로 걸렸는지 확인한다
|
||
|
||
## 3-1. 파드가 새 이미지로 돌고 있나
|
||
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab get pods -o custom-columns=\
|
||
NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready,\
|
||
RESTARTS:.status.containerStatuses[0].restartCount | grep keycloak
|
||
```
|
||
**실측** — [`followup/01`](../../evidence/followup/01-d2-forward-upgrade.txt)
|
||
```
|
||
quay.io/keycloak/keycloak:26.7.3
|
||
keycloak-0 1/1 Running restarts=0
|
||
keycloak-1 1/1 Running restarts=0
|
||
```
|
||
|
||
**어디를 봐야 하는가** — **`RESTARTS` 가 `0`.** 여기가 0 인 것이 중요하다.
|
||
교체는 **새 파드를 만드는 것**이지 같은 파드를 재시작하는 것이 아니다.
|
||
`RESTARTS` 가 올라가 있으면 새 파드가 기동에 실패해 재시작을 반복하는 것이다.
|
||
|
||
**확인** — 실제로 새 파드인지는 나이로 본다
|
||
```bash
|
||
sudo kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||
```
|
||
**실측** — 첫 실행의 롤포워드 직후
|
||
```
|
||
keycloak-0 1/1 Running 0 10m
|
||
keycloak-1 1/1 Running 0 28s
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `AGE`. 하나씩 갈리므로 **나이가 다르다.** 둘 다 방금
|
||
생긴 나이면 동시에 갈린 것이고, 그건 무중단이 아니다.
|
||
|
||
## 3-2. 버전이 정말 바뀌었나 — 파드가 자기 입으로 말하게 한다
|
||
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab logs keycloak-0 | grep -i 'Keycloak 26' | tail -1
|
||
```
|
||
**실측**
|
||
```
|
||
Keycloak 26.7.3
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 로그가 말하는 판. 이미지 태그와 다르면 **태그가
|
||
재사용된 것**이다(같은 태그가 다른 내용을 가리키는 경우).
|
||
|
||
---
|
||
|
||
# 4. 효과를 관찰한다
|
||
|
||
## 4-1. 끊겼나
|
||
|
||
**확인**
|
||
```bash
|
||
tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
|
||
```
|
||
**실측** — [`followup/01`](../../evidence/followup/01-d2-forward-upgrade.txt)
|
||
```
|
||
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
|
||
...
|
||
200 응답: 87 회
|
||
비200 : 0
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 줄이 하나뿐이고 그 값이 `200` 인가.
|
||
|
||
**이 결과가 의미하는 것** — **정방향 업그레이드는 무중단이었다.** 87회 요청이
|
||
전부 200 이다. 파드가 하나씩 갈리는 동안 남은 파드가 받았다.
|
||
|
||
> **「무중단」은 관측 해상도에 달려 있다.** 이건 1초 간격·3초 타임아웃으로
|
||
> 잰 결과다. 더 촘촘히 보면 더 보일 수 있다 — 실제로 D-4 에서 0.2초 간격으로
|
||
> 재니 다른 것이 보였다.
|
||
|
||
## 4-2. 그림으로도 남아 있다
|
||
|
||
Grafana 스크린샷이 증거에 있다 —
|
||
[`d2-upgrade-window.png`](../../evidence/d2-version-upgrade/d2-upgrade-window.png).
|
||
|
||
**무엇이 보이나** — `cluster_size` 가 **2 → 1 → 2 를 두 번** 반복하고, 파드별
|
||
`up` 시계열이 끝나고 새 시계열이 시작된다.
|
||
|
||
**어디를 봐야 하는가** — **2 → 1 → 2 가 두 번**인 것. 파드가 둘이므로 교체도
|
||
두 번이고, 그때마다 클러스터가 잠시 한 명이 된다. **한 번만 보이면 두 파드가
|
||
동시에 갈린 것이다.**
|
||
|
||
## 4-3. ★ 스키마가 움직였나 — 이 실험의 판정
|
||
|
||
**확인** — 1-3 과 **똑같은 명령**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||
"select count(*) from databasechangelog"
|
||
```
|
||
**실측** — [`followup/01`](../../evidence/followup/01-d2-forward-upgrade.txt)
|
||
```
|
||
마이그레이션 후: 210 (전: 210)
|
||
세션 후: 3 (전: 3)
|
||
```
|
||
|
||
**어디를 봐야 하는가** — **전과 후가 같은가.**
|
||
|
||
**이 결과가 의미하는 것**
|
||
|
||
| 결과 | 뜻 | 되돌리는 법 |
|
||
|---|---|---|
|
||
| **행 수가 그대로** | 스키마가 안 움직였다 | **태그만 되돌리면 된다** |
|
||
| 행 수가 늘었다 | 새 changeset 이 적용됐다 | **덤프 복구 + 태그 되돌리기** |
|
||
|
||
26.7.0 → 26.7.3 은 **패치 릴리스라 스키마가 그대로**였다. 그래서 롤백이
|
||
가능하다는 가설이 섰고, 다음 절에서 시험한다.
|
||
|
||
## 4-4. 가설 시험 — 같은 스키마에서 롤백해 본다
|
||
|
||
**되돌리기** — 이 절 자체가 되돌리기다. 다시 올리려면 태그를 26.7.3 으로.
|
||
|
||
**하기**
|
||
```bash
|
||
date '+%H:%M:%S 롤백'
|
||
sudo kubectl -n keycloak-lab set image statefulset/keycloak \
|
||
keycloak=quay.io/keycloak/keycloak:26.7.0
|
||
sudo kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=600s
|
||
```
|
||
**실측** — [`followup/02`](../../evidence/followup/02-d2-rollback-same-schema.txt)
|
||
```
|
||
시작: 15:25:08
|
||
partitioned roll out complete: 2 new pods have been updated...
|
||
완료: 15:25:53
|
||
|
||
keycloak-0 1/1 Running restarts=0
|
||
keycloak-1 1/1 Running restarts=0
|
||
Keycloak 26.7.0
|
||
마이그레이션: 210
|
||
세션: 3
|
||
```
|
||
|
||
**어디를 봐야 하는가** — **파드가 뜬다.** 이게 가설의 답이다.
|
||
|
||
**이 결과가 의미하는 것** — **스키마가 안 바뀌었으면 태그를 되돌리는 것으로
|
||
충분하다.** 마이그레이션 210 그대로, 세션 3 그대로, 재시작 0.
|
||
|
||
### 전환 순간의 `000` 한 번을 오해하지 않는다
|
||
|
||
**확인**
|
||
```bash
|
||
tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
|
||
grep -n '000' /tmp/d2-avail.txt
|
||
```
|
||
**실측** — [`followup/02`](../../evidence/followup/02-d2-rollback-same-schema.txt)
|
||
```
|
||
200 응답: 43 회 / 비200: 1
|
||
|
||
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
|
||
200 200 200 200 000 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
|
||
200 200 200 200
|
||
비200 값: 000
|
||
|
||
=== 대조: 정방향 업그레이드 때는 ===
|
||
200: 87 / 비200: 0
|
||
```
|
||
|
||
**어디를 봐야 하는가** — `000` 이다. **`500` 도 `502` 도 `503` 도 아니다.**
|
||
|
||
`000` 은 **curl 이 HTTP 상태 코드를 하나도 못 받았다**는 뜻이며, 여기서는
|
||
`--max-time 3` 을 넘긴 것이다. 서버가 오류를 돌려준 것이 아니라 **3초 안에
|
||
응답이 안 왔다.**
|
||
|
||
**이 결과가 의미하는 것** — 파드 전환 순간 요청 하나가 3초를 넘겼다. 정방향에서
|
||
0회, 역방향에서 1회다. **끊긴 것과 느린 것은 다르고, 그 구별은 코드가 아니라
|
||
`--max-time` 을 알고 있어야 된다.**
|
||
|
||
---
|
||
|
||
# 5. ★ 대조 — 스키마가 움직인 방향에서는 무슨 일이 나는가 (선택)
|
||
|
||
**여기부터는 일부러 실패시킨다.** 앞의 4절까지로 이 실험의 판정은 끝났다.
|
||
이 절은 「행 수가 바뀌었을 때」가 실제로 어떤 모양인지 보려는 것이다.
|
||
|
||
**되돌리기 — 먼저 읽는다**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab set image statefulset/keycloak \
|
||
keycloak=quay.io/keycloak/keycloak:26.7.0
|
||
```
|
||
|
||
**하기**
|
||
```bash
|
||
date '+%H:%M:%S 26.0 으로 내린다'
|
||
sudo kubectl -n keycloak-lab set image statefulset/keycloak \
|
||
keycloak=quay.io/keycloak/keycloak:26.0
|
||
```
|
||
|
||
**확인** — 이번에는 `rollout status` 로 기다리지 말고 **눈으로 본다**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab get pods -w
|
||
```
|
||
**실측** — [`02-rollback-attempt.txt`](../../evidence/d2-version-upgrade/02-rollback-attempt.txt)
|
||
```
|
||
시각: 15:02:20
|
||
statefulset.apps/keycloak image updated
|
||
+20초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
|
||
+40초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
|
||
+60초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
|
||
+80초 keycloak-0:Running(1/1) keycloak-1:Error(0/1)
|
||
+100초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
|
||
+120초 keycloak-0:Running(1/1) keycloak-1:Error(0/1)
|
||
+140초 keycloak-0:Running(1/1) keycloak-1:CrashLoopBackOff(0/1)
|
||
+160초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
|
||
```
|
||
|
||
**어디를 봐야 하는가** — 두 가지다.
|
||
|
||
- `keycloak-1` 이 `Running(0/1) → Error → CrashLoopBackOff` 를 오간다.
|
||
**`Running` 인데 `0/1` 인 상태를 「떴다」로 읽으면 안 된다** — 컨테이너
|
||
프로세스는 살아 있지만 readiness 를 통과하지 못한 것이고, 곧 죽는다.
|
||
- **`keycloak-0` 은 내내 `1/1` 이다.** StatefulSet 이 안 건드렸다.
|
||
|
||
`Ctrl-C` 로 빠져나온다.
|
||
|
||
## 5-1. 왜 실패했는지 물어본다
|
||
|
||
**확인**
|
||
```bash
|
||
sudo kubectl -n keycloak-lab logs keycloak-1 | grep -iE 'liquibase|changeset|validation'
|
||
```
|
||
**실측** — [`03-roll-forward.txt`](../../evidence/d2-version-upgrade/03-roll-forward.txt)
|
||
```
|
||
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: liquibase.exception.ValidationFailedException: Validation Failed:
|
||
1 changesets check sum
|
||
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Validation Failed:
|
||
1 changesets check sum
|
||
```
|
||
|
||
**어디를 봐야 하는가** — **`1 changesets check sum`.** 개수가 1이다.
|
||
|
||
**이 결과가 의미하는 것** — 26.7.0 이 적용한 changeset 하나를 26.0 도 알고
|
||
있는데, **정의가 다르다.** 같은 changeset 이 버전 사이에 수정된 것이다.
|
||
Liquibase 는 스키마를 반쯤 아는 상태로 서비스하느니 **기동 자체를 거부**한다.
|
||
|
||
> 파드가 이미 죽어서 로그가 안 나오면 **직전 컨테이너의 로그**를 본다.
|
||
> ```bash
|
||
> sudo kubectl -n keycloak-lab logs keycloak-1 --previous
|
||
> ```
|
||
|
||
## 5-2. 그런데 서비스는 살아 있다
|
||
|
||
**확인**
|
||
```bash
|
||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||
sudo kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
|
||
-o custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
|
||
sudo kubectl -n keycloak-lab get statefulset keycloak
|
||
```
|
||
**실측** — [`03-roll-forward.txt`](../../evidence/d2-version-upgrade/03-roll-forward.txt)
|
||
```
|
||
https://auth.hyeonworks.com/realms/master HTTP 200
|
||
ready 주소: [10.42.1.140] ← 한 파드만
|
||
statefulset desired/ready/updated: 2 / 1 / 1
|
||
```
|
||
|
||
**어디를 봐야 하는가** — ready 주소가 **하나**, 그리고 `desired/ready/updated`
|
||
가 **2 / 1 / 1**.
|
||
|
||
**이 결과가 의미하는 것** — **StatefulSet 의 롤링 업데이트가 사고를 절반에서
|
||
멈춰줬다.**
|
||
|
||
```
|
||
keycloak-1 을 26.0 으로 → 기동 실패 → Ready 가 안 됨
|
||
└─ StatefulSet 은 keycloak-0 을 건드리지 않는다
|
||
└─ keycloak-0 (26.7.0) 이 계속 서비스한다
|
||
```
|
||
|
||
| replica 1 이었다면 | |
|
||
|---|---|
|
||
| 유일한 파드가 CrashLoopBackOff | **전면 장애** |
|
||
| 되돌리려면 사람이 개입 | 그동안 계속 다운 |
|
||
|
||
**A-8 에서 「무중단은 replica ≥ 2 와 readiness 의 조합」이라고 썼는데, 여기서는
|
||
그 조합이 잘못된 배포를 절반에서 멈춰줬다.**
|
||
|
||
## 5-3. 실패한 기동이 스키마를 건드렸나
|
||
|
||
**확인** — 세 번째로 같은 명령
|
||
```bash
|
||
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||
"select count(*) from databasechangelog"
|
||
```
|
||
**실측**
|
||
```
|
||
realms|clients|migrations|sessions = 2|15|210|4
|
||
```
|
||
|
||
**어디를 봐야 하는가** — **210 그대로.**
|
||
|
||
**이 결과가 의미하는 것** — **Liquibase 가 검증 단계에서 멈췄으므로 스키마를
|
||
건드리지 못했다.** 그래서 이 사고는 「태그만 되돌리면 되는」 쪽에 남았다.
|
||
|
||
**여기가 4번과 5번을 가르는 지점이다.**
|
||
|
||
```
|
||
✔ Liquibase 가 검증에서 멈췄다 → 이미지만 되돌리면 끝
|
||
✘ 이미 적용한 뒤였다 → DB 복구(D-1)까지 해야 한다
|
||
```
|
||
|
||
---
|
||
|
||
# 6. 복구
|
||
|
||
**하기**
|
||
```bash
|
||
date '+%H:%M:%S 복귀'
|
||
sudo kubectl -n keycloak-lab set image statefulset/keycloak \
|
||
keycloak=quay.io/keycloak/keycloak:26.7.0
|
||
sudo kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=600s
|
||
```
|
||
**실측** — [`03-roll-forward.txt`](../../evidence/d2-version-upgrade/03-roll-forward.txt)
|
||
```
|
||
statefulset.apps/keycloak image updated
|
||
partitioned roll out complete: 2 new pods have been updated...
|
||
keycloak-0 1/1 Running 0 10m
|
||
keycloak-1 1/1 Running 0 28s
|
||
|
||
realms|clients|migrations|sessions = 2|15|210|4
|
||
외부 진입점 HTTP 200
|
||
```
|
||
|
||
> **`kubectl rollout undo statefulset/keycloak` 도 있다.** 이 실험은 쓰지 않았고
|
||
> (**미검증**), 쓰더라도 **되돌아가는 것은 이미지뿐이다.** 스키마가 움직였다면
|
||
> undo 도 같은 벽에 부딪힌다.
|
||
|
||
## 6-1. 원상복구 확인표
|
||
|
||
| 항목 | 명령 | 돌아왔을 때 |
|
||
|---|---|---|
|
||
| 태그 | `get statefulset keycloak -o jsonpath='{.spec.template.spec.containers[0].image}'` | 시작할 때의 태그 |
|
||
| 파드 | `get pods -o wide \| grep keycloak` | 둘 다 `1/1 Running`, `RESTARTS 0` |
|
||
| 클러스터 | `logs keycloak-0 \| grep ISPN000094 \| tail -1` | 멤버 `(2)` |
|
||
| **마이그레이션** | `psql -tAc "select count(*) from databasechangelog"` | **210 — 시작할 때와 같다** |
|
||
| 세션 | `psql -tAc "select count(*) from offline_user_session"` | 시작할 때와 같다 |
|
||
| Service | `get endpointslice -l kubernetes.io/service-name=keycloak` | ready 주소 **둘** |
|
||
| 밖 | `curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master` | `200` |
|
||
| 폴링 | `jobs` | 남아 있으면 `kill %1` |
|
||
| 덤프 | `ls -l /tmp/pre-upgrade.sql` | 남겨 둔다 |
|
||
|
||
---
|
||
|
||
# 7. 그래서 업그레이드 계획은 어떻게 쓰는가
|
||
|
||
```
|
||
✘ "문제가 생기면 이미지 태그를 되돌린다"
|
||
└─ 스키마가 이미 바뀌었으면 옛 버전이 안 뜬다
|
||
|
||
✔ "업그레이드 전에 databasechangelog 를 세어 두고,
|
||
바뀌었으면 백업에서 DB 를 되돌린 뒤 태그를 되돌린다"
|
||
```
|
||
|
||
| 단계 | |
|
||
|---|---|
|
||
| 1 | **백업**(D-1). 스키마가 움직인 뒤에는 이것만이 되돌리기 수단이다 |
|
||
| 2 | **`databasechangelog` 행 수를 적어 둔다** — 나중에는 못 잰다 |
|
||
| 3 | 태그 변경 |
|
||
| 4 | **첫 파드만 관찰** — StatefulSet 이 멈춰준다 |
|
||
| 5 | 행 수를 다시 센다. **그대로면** 태그만 되돌려도 된다 |
|
||
| 6 | **늘었으면** DB 복구 + 태그 되돌리기 |
|
||
|
||
## 이 실험이 확인한 것과 못 한 것
|
||
|
||
| | |
|
||
|---|---|
|
||
| ✔ 정방향(26.7.0 → 26.7.3) 무중단 | 87회 전부 200 |
|
||
| ✔ 같은 스키마에서 롤백 가능 | 43/1, 그 1은 `--max-time` 타임아웃 |
|
||
| ✔ 스키마가 바뀐 방향은 기동 거부 | `1 changesets check sum` |
|
||
| ✔ 실패가 절반에서 격리된다 | StatefulSet + readiness |
|
||
| ✔ 실패한 기동은 스키마를 안 건드린다 | 210 그대로 |
|
||
| ✘ **스키마가 실제로 늘어나는 업그레이드** | **이 실험대에서는 재현하지 못했다.** 26.7.x 사이에는 변경이 없다 |
|
||
| ✘ 마이그레이션 도중 장애 | 스키마 변경 중에 죽으면? |
|
||
| ✘ 대규모 마이그레이션 시간 | 데이터가 작아 순식간이다 |
|
||
|
||
> **가장 중요한 미검증이 첫 줄이다.** 「행 수가 늘면 태그로 못 돌아온다」는
|
||
> **역방향(26.0)에서 관측한 실패를 근거로 한 추론**이며, 실제로 행 수가 늘어난
|
||
> 뒤 되돌려 본 적은 없다. 메이저 업그레이드를 할 때 이 실험을 다시 한다.
|
||
|
||
---
|
||
|
||
# 막히면
|
||
|
||
| 증상 | 원인 | 확인 |
|
||
|---|---|---|
|
||
| `rollout status` 가 안 끝난다 | **첫 파드가 안 뜬다.** StatefulSet 이 기다린다 | 다른 터미널에서 `get pods -w` — 5절 |
|
||
| 파드가 `Running` 인데 `0/1` | 프로세스는 살아 있고 readiness 미통과 | `logs` 를 본다. 「떴다」로 읽지 않는다 |
|
||
| 로그가 안 나온다 | 파드가 이미 죽었다 | `logs keycloak-1 --previous` |
|
||
| **업그레이드 전 행 수를 안 적었다** | 그 값은 이제 DB 에 없다 | 덤프에서 복원한다 — 아래 |
|
||
| 비200 이 `000` 이다 | 서버 오류가 아니라 **`--max-time` 타임아웃** | `--max-time` 값을 늘려 다시 재 본다 — 4-4 |
|
||
| `kubectl get endpoints` 가 경고를 찍는다 | v1.33+ 에서 deprecated. **실측으로 이 경고를 봤다** | `get endpointslice -l kubernetes.io/service-name=...` |
|
||
| 두 파드가 동시에 갈렸다 | `podManagementPolicy: Parallel` | `get statefulset keycloak -o yaml \| grep podManagement` |
|
||
| 새 태그를 못 찾는다 | 레지스트리에서 확인 안 했다 | 1-6 |
|
||
|
||
**업그레이드 전 행 수를 안 적었을 때** — 덤프 안에 그 테이블이 통째로 들어
|
||
있다. **미검증**
|
||
```bash
|
||
sed -n '/^COPY public.databasechangelog /,/^\\\.$/p' /tmp/pre-upgrade.sql | wc -l
|
||
```
|
||
**어디를 봐야 하는가** — 나온 수에서 **2를 뺀다**(`COPY` 줄과 `\.` 줄).
|
||
이게 백업 시점의 행 수다. **D-1 의 덤프가 여기서 한 번 더 값을 한다.**
|
||
|
||
---
|
||
|
||
# 다음
|
||
|
||
| 실험 | D-2 가 남긴 것 |
|
||
|---|---|
|
||
| [D-1](d1-backup-restore.md) 백업·복구 | **롤백 = 백업 복구**인 경우가 있다. 태그만 되돌리는 계획은 반쪽이다 |
|
||
| [D-3](d3-secret-management.md) 비밀 관리 | 업그레이드할 때 Secret 도 같이 검토된다 |
|
||
| [A-8](../../experiment-a8-rolling-restart.md) 롤링 재시작 | **replica ≥ 2 가 잘못된 배포를 절반에서 멈춘다** |
|
||
| 운영 | **판정은 버전 번호가 아니라 `databasechangelog` 의 행 수다** |
|
||
| 전부 | **재기 전에 못 재는 값을 먼저 적어 둔다.** 업그레이드 전 행 수가 그것이다 |
|