Compare commits

...
Author SHA1 Message Date
DongHyeonkaandClaude Opus 5 df140ab218 docs: D-2 — rolling back the image does not roll back the schema
Downgrading from 26.7.0 to 26.0 fails with liquibase ValidationFailedException on a changeset checksum, which is stricter than an unknown migration: the old version knows the changeset but its definition differs. The pod goes CrashLoopBackOff and never starts.

The StatefulSet stopped the rollout at the first pod, so the other kept serving and the front door stayed at 200, which replica 1 would not have done. The failed start never touched the schema, so restoring the image was enough; had the migration already applied, the D-1 database restore would have been the only way back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:05:36 +09:00
DongHyeonkaandClaude Opus 5 df5af95cb3 docs: D-1 — an empty database still answered 200
Dropping the schema left Keycloak serving realm metadata and JWKS from its Infinispan cache, so the front door stayed at 200 while only the paths that read the database failed. That is a different shape from A-2, where the connection itself broke and readiness pulled the pods out of the Service; here the connection is fine and the tables are simply gone, which the health check does not notice.

Restoring the pg_dump took one second with zero errors and no pod restart, and the row counts matched the backup exactly, sessions included. The real RPO is the backup interval plus the synchronous_commit loss measured in A-3, and this dump sits in the host's /tmp, which is the same failure domain as the thing it protects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:59:56 +09:00
10 changed files with 598 additions and 0 deletions
@@ -0,0 +1,15 @@
=== 백업 전 상태 ===
realms|clients|users|sessions|authclients = 2|15|2|3|1
=== pg_dump — 전체 덤프 ===
시작: 14:59:30
완료: 14:59:30
크기: 394945 bytes (6956 줄)
포함된 테이블 수: 101
=== 덤프에 세션이 들어 있는가 ===
offline_user_session 언급: 13
COPY public.offline_user_session (user_session_id, user_id, realm_id, created_on, offline_flag, data, last_session_refre
E1q5xI7tt4U_WhZpW7rEPIF2 48b37d33-8419-49aa-9b5b-7731975be50c 7845f394-723a-4d07-b530-c7416b2e1d31 1788500836 0 {"ipAddr
2ap3DyRiBF8OdMiqCodsJ0mp 48b37d33-8419-49aa-9b5b-7731975be50c 7845f394-723a-4d07-b530-c7416b2e1d31 1788501029 0 {"ipAddr
Zsk4QcgXf_qgyMKzde5AG-Fz 48b37d33-8419-49aa-9b5b-7731975be50c 7845f394-723a-4d07-b530-c7416b2e1d31 1788501263 0 {"ipAddr
@@ -0,0 +1,17 @@
=== ★ 파괴 — 스키마를 통째로 지운다 ===
시각: 14:59:47
DROP SCHEMA
CREATE SCHEMA
남은 테이블: 0
=== 서비스 영향 ===
https://auth.hyeonworks.com/realms/master HTTP 200
https://app1.hyeonworks.com/ HTTP 200
bff-555df79c97-6j86w 1/1 Running 0 49m
bff-555df79c97-vgg6g 1/1 Running 0 49m
keycloak-0 1/1 Running 0 4m15s
keycloak-1 1/1 Running 0 4m38s
=== Keycloak 이 무엇을 말하는가 ===
2026-09-04 05:58:02,598 WARN [org.keycloak.jgroups.protocol.KEYCLOAK_JDBC_PING2] (blocking-thread--p3-t2) Failed to fetch the cluster members from the database.: org.postgresql.ut
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2904)
@@ -0,0 +1,29 @@
=== 무엇이 실제로 깨지는가 ===
/.well-known/openid-configuration HTTP 500
/protocol/openid-connect/certs HTTP 200
토큰 발급 (DB 쓰기 필요) HTTP 400
=== ★ 복구 — 덤프에서 되돌린다 ===
시작: 15:00:12
완료: 15:00:13
오류 줄: 0
=== 복구 후 데이터 ===
realms|clients|users|sessions|authclients = 2|15|2|3|1
=== 복구 직후 — 재시작 없이 되는가 ===
+15초 well-known=200 토큰발급=200
→ 재시작 없이 회복
=== 복구 전 세션이 살아났는가 ===
user_session_id | realm
--------------------------+-------------------
E1q5xI7tt4U_WhZpW7rEPIF2 | master
2ap3DyRiBF8OdMiqCodsJ0mp | master
Zsk4QcgXf_qgyMKzde5AG-Fz | master
vsDgCVo12-qX0CC63ZmYzbYF | keycloak-patterns
(4 rows)
=== 파드 재시작 횟수 ===
keycloak-0 restarts=0
keycloak-1 restarts=0
+16
View File
@@ -0,0 +1,16 @@
# D-1 — 백업·복구 리허설 증거
2026-09-04 16:5517:05 KST
해설: [`docs/experiment-d1-backup-restore.md`](../../experiment-d1-backup-restore.md)
| 파일 | 무엇을 보여주는가 |
|---|---|
| `01-backup.txt` | `pg_dump --clean --if-exists` — 395KB · 101 테이블 · **세션 데이터 포함** |
| `02-destruction.txt` | `DROP SCHEMA public CASCADE` → 테이블 0개. **그런데 외부는 `HTTP 200`** — Keycloak 이 realm 캐시로 서빙한다 |
| `03-restore.txt` | 깨지는 것과 안 깨지는 것(`certs` 200 / `well-known` 500 / 토큰 400) · **복구 1초 · 오류 0건 · 데이터 완전 일치 · 재시작 0회** |
## 핵심 세 줄
1. **데이터베이스를 통째로 비웠는데 서비스가 200 을 냈다.** 헬스체크는 "DB 가 살아 있다"만 보고 "데이터가 있다"는 안 본다.
2. **복구는 1초, 오류 0건, 재시작 불필요.** 절차가 맞다는 것은 확인됐다.
3. **RPO 는 두 겹이다** — 백업 주기 + A-3 에서 측정한 `synchronous_commit OFF` 손실. 그리고 이번 덤프는 호스트의 `/tmp` 에 있어 **같은 장애 도메인**이다.
@@ -0,0 +1,9 @@
=== D-1 의 교훈: 업그레이드 전에 백업한다 ===
백업: 396333 bytes
=== 현재 버전과 스키마 상태 ===
quay.io/keycloak/keycloak:26.7.0
총 마이그레이션 수: 210
=== 로그인 상태 만들기 (업그레이드 후 살아남는지 볼 것) ===
현재 세션: 4
@@ -0,0 +1,17 @@
=== ★ 롤백 시도: 26.7.0 → 26.0 ===
시각: 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)
=== 새 파드가 무엇을 말하는가 ===
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Failed to start server in (production) mode
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: liquibase.exception.ValidationFailedException: Validation Failed:
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Validation Failed:
2026-09-04 06:03:25,877 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) For more details run the same command passing the '--verbose' option. Also you can use '--help' to see the detai
@@ -0,0 +1,20 @@
=== 서비스는 살아 있는가 (StatefulSet 롤링이 막아줬다) ===
https://auth.hyeonworks.com/realms/master HTTP 200
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
ready 주소: [10.42.1.140]sed: -e expression #1, char 27: unknown option to 's'
=== Liquibase 오류 상세 ===
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
=== ★ 앞으로 되돌린다 (26.7.0) ===
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
@@ -0,0 +1,16 @@
# D-2 — 버전 업그레이드 증거
2026-09-04 17:0517:15 KST
해설: [`docs/experiment-d2-version-upgrade.md`](../../experiment-d2-version-upgrade.md)
| 파일 | 무엇을 보여주는가 |
|---|---|
| `01-pre-upgrade.txt` | 백업 396KB · 이미지 26.7.0 · **마이그레이션 210건** · 세션 4 |
| `02-rollback-attempt.txt` | 26.0 으로 내리자 `Running(0/1) → Error → CrashLoopBackOff`. **`liquibase.exception.ValidationFailedException`** |
| `03-roll-forward.txt` | **서비스는 `HTTP 200` 유지**(ready 주소 1개) · 오류 원인 `1 changesets check sum` · 26.7.0 복귀 후 마이그레이션 210·세션 4 그대로 |
## 핵심 세 줄
1. **롤백은 안 된다.** 체크섬이 안 맞아 Liquibase 가 기동 자체를 거부한다 — "모르는 변경"이 아니라 "아는 변경인데 정의가 다르다".
2. **StatefulSet 이 사고를 절반에서 멈춰줬다.** 한 파드가 남아 외부 200 을 유지했다. replica 1 이었다면 전면 장애다.
3. **실패한 기동은 스키마를 안 건드렸다.** 그래서 이미지만 되돌려도 복구됐다 — 이미 적용된 뒤였다면 DB 복구(D-1)가 필요하다.
+246
View File
@@ -0,0 +1,246 @@
# D-1 — 백업이 있다와 복구해봤다는 다르다
브랜치 `feature/keycloak-d1-backup-restore` ·
증거 [`docs/evidence/d1-backup-restore/`](evidence/d1-backup-restore/) ·
2026-09-04 16:5517:05 KST
선행: [`A-2`](experiment-a2-database-loss.md) · [`A-3`](experiment-a3-database-crash.md)
---
## 0. 결론부터
| 측정 | 값 |
|---|---|
| 덤프 크기 / 시간 | **395KB · 1초 미만** (101개 테이블) |
| 복구 시간 | **1초** (`15:00:12 → 15:00:13`), **오류 0건** |
| 서비스 회복 | **재시작 없이 15초 이내** (`restarts=0`) |
| 데이터 일치 | **완전 일치** — realms 2 / clients 15 / users 2 / sessions 3 / authclients 1 |
| **RTO** | 약 **30초** (파괴 감지부터 서비스 복귀까지) |
| **RPO** | **마지막 덤프 시점** + A-3 의 `synchronous_commit OFF` 손실 |
**그리고 예상 못 한 것 — 스키마를 통째로 지웠는데 서비스가 `200` 을 계속 냈다.**
---
## 1. 백업
```bash
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
--clean --if-exists > /tmp/keycloak-backup.sql
```
```
크기: 394945 bytes (6956 줄)
CREATE TABLE: 101 개
offline_user_session 언급: 13
```
**세션도 덤프에 들어간다.**
```
COPY public.offline_user_session (user_session_id, user_id, realm_id, created_on, offline_flag, data, ...)
E1q5xI7tt4U_WhZpW7rEPIF2 48b37d33-... 7845f394-... 1788500836 0 {"ipAddr...
```
| 옵션 | 뜻 |
|---|---|
| `--clean` | 복구 시 기존 객체를 **DROP 하고** 다시 만든다 |
| `--if-exists` | 없는 객체를 DROP 할 때 오류를 내지 않는다 |
**두 옵션이 없으면 "이미 존재한다" 오류가 쏟아진다.**
---
## 2. 파괴
```bash
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
```
```
DROP SCHEMA
CREATE SCHEMA
남은 테이블: 0
```
### ★ 그런데 서비스가 살아 있었다
```
https://auth.hyeonworks.com/realms/master HTTP 200
https://app1.hyeonworks.com/ HTTP 200
keycloak-0 / keycloak-1 1/1 Running
```
**데이터베이스가 통째로 비었는데 `200` 이다.**
Keycloak 이 realm 정보를 **Infinispan `realms` 캐시**에서 서빙하기 때문이다
(A-0 에서 그 캐시에 57개 엔트리가 있는 것을 봤다).
### 무엇이 깨지고 무엇이 안 깨지는가
```
/protocol/openid-connect/certs HTTP 200 ← realm 키는 캐시에 있다
/.well-known/openid-configuration HTTP 500 ← 이건 DB 를 본다
토큰 발급 HTTP 400
```
**부분적으로만 깨진다.** 헬스체크는 통과하고, 일부 엔드포인트는 정상이며,
**로그인만 안 된다.**
```
KEYCLOAK_JDBC_PING2: Failed to fetch the cluster members from the database
```
> **A-2(DB 프로세스 정지)와 다른 모양이다.** 거기서는 커넥션이 아예 안 돼
> readiness 가 DOWN 이 되고 전 파드가 Service 에서 빠졌다.
> **여기서는 커넥션은 되고 테이블만 없다** — 헬스체크가 통과해버린다.
>
> **"DB 가 살아 있다"와 "데이터가 있다"는 다르다.** 헬스체크는 앞의 것만 본다.
---
## 3. 복구
```bash
kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak \
< /tmp/keycloak-backup.sql
```
```
시작: 15:00:12
완료: 15:00:13
오류 줄: 0
```
**1초, 오류 없음.**
```
복구 후: realms 2 | clients 15 | users 2 | sessions 3 | authclients 1
백업 시: realms 2 | clients 15 | users 2 | sessions 3 | authclients 1
```
**완전히 일치한다.**
### 서비스는 재시작 없이 돌아왔다
```
+15초 well-known=200 토큰발급=200
→ 재시작 없이 회복
keycloak-0 restarts=0
keycloak-1 restarts=0
```
**커넥션 풀이 이미 붙어 있었으므로 테이블이 돌아오자마자 동작했다.**
A-2 에서 본 것과 같은 자가 회복이다.
### 세션도 살아났다
```
user_session_id | realm
--------------------------+-------------------
E1q5xI7tt4U_WhZpW7rEPIF2 | master
2ap3DyRiBF8OdMiqCodsJ0mp | master
Zsk4QcgXf_qgyMKzde5AG-Fz | master
vsDgCVo12-qX0CC63ZmYzbYF | keycloak-patterns
```
**`persistent-user-sessions` 덕분에 세션이 백업 대상이 된다** (A-0).
volatile 이었다면 세션은 애초에 DB 에 없으므로 **복구해도 전원 재로그인**이다.
---
## 4. RTO 와 RPO
```
14:59:47 파괴
15:00:12 복구 시작
15:00:13 복구 완료
~15:00:28 서비스 정상 확인
RTO ≈ 30초 (이 규모에서는 대부분이 사람의 판단 시간이다)
```
### RPO 는 두 겹이다
```
① 마지막 덤프 이후의 모든 변경 ← 백업 주기가 정한다
② A-3 에서 측정한 synchronous_commit 손실 ← 수백 ms
실제 RPO = ① + ②
```
**A-3 에서 "153건 중 4건 유실"을 측정한 것이 여기에 더해진다.**
백업 주기만 보고 RPO 를 말하면 ②를 빠뜨린다.
### 이 실험대의 규모는 현실적이지 않다
| | 이 실험대 | 운영 |
|---|---|---|
| 덤프 크기 | 395KB | GB~TB |
| 복구 시간 | 1초 | 분~시간 |
| 세션 수 | 3 | 수만 |
**복구가 1초인 것은 데이터가 작기 때문**이며, **절차가 맞다는 것만 확인된다.**
시간은 규모에 따라 완전히 달라진다.
---
## 5. 이 실험이 검증한 것과 못 한 것
| | |
|---|---|
| ✔ 덤프에 필요한 것이 다 들어간다 | realm·client·user·session·authorized client |
| ✔ 복구 절차가 동작한다 | `--clean --if-exists` 로 오류 0 |
| ✔ 서비스가 자가 회복한다 | 재시작 불필요 |
| ✘ **노드가 죽은 경우** | A-4 에서 본 대로 **PVC 가 노드에 묶여 있다.** 노드가 안 돌아오면 덤프가 유일한 길인데, **덤프를 어디에 두느냐**가 문제가 된다 |
| ✘ 대규모 복구 시간 | 데이터가 작아 측정 의미가 없다 |
| ✘ 백업 자동화·보존·검증 | 이번엔 손으로 한 번 떴다 |
> **가장 중요한 미검증 항목이 "덤프를 어디에 두는가" 다.**
> 이번 덤프는 `test-server:/tmp` 에 있다. **호스트가 죽으면 같이 사라진다.**
> A-4 에서 PVC 가 노드에 묶인 것을 봤듯, **백업도 같은 장애 도메인에 있으면
> 백업이 아니다.**
---
## 6. 재현 절차 (명령어)
```bash
# 1. 백업 — --clean --if-exists 가 없으면 복구 때 오류가 쏟아진다
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
--clean --if-exists > keycloak-backup.sql
# 2. 무엇이 들어갔는지 확인 (세션이 있어야 한다)
grep -c '^CREATE TABLE' keycloak-backup.sql
grep -A3 'COPY public.offline_user_session' keycloak-backup.sql
# 3. 파괴
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
# 4. ★ 무엇이 깨지는지 확인 — 전부 깨지지 않는다
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/keycloak-patterns/.well-known/openid-configuration
# 5. 복구
kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak < keycloak-backup.sql
# 6. 데이터 대조 — 백업 시점의 수치와 같아야 한다
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
"select (select count(*) from realm), (select count(*) from client),
(select count(*) from user_entity),
(select count(*) from offline_user_session where offline_flag='0')"
```
---
## 7. 다음 실험에 남기는 것
| 실험 | 이 실험이 준 것 |
|---|---|
| **D-2** 버전 업그레이드 | **백업이 전제다.** 스키마 마이그레이션은 되돌리기 어렵다 |
| 운영 | **덤프를 다른 장애 도메인에 둔다** |
| 관측 | **"DB 가 살아 있다"만 보는 헬스체크는 빈 DB 를 통과시킨다** |
+213
View File
@@ -0,0 +1,213 @@
# D-2 — 버전을 올리고 내릴 때 무엇이 일어나는가
브랜치 `feature/keycloak-d2-version-upgrade` ·
증거 [`docs/evidence/d2-version-upgrade/`](evidence/d2-version-upgrade/) ·
2026-09-04 17:0517:15 KST
선행: [`D-1`](experiment-d1-backup-restore.md) — **백업이 전제다** ·
[`A-8`](experiment-a8-rolling-restart.md) — 롤링 재시작이 안전하다는 것이 전제
---
## 0. 결론부터
| 확인 | 결과 |
|---|---|
| **롤백이 되는가** | **★ 안 된다.** `liquibase ValidationFailedException: 1 changesets check sum` |
| 그때 서비스는 | **★ 살아 있다.** 한 파드가 남아 외부 `200` |
| 앞으로 되돌리기 | **된다.** 정상 복구, 데이터 무사 |
| 세션 | **유지** (4개 그대로) |
**"롤백 계획"을 세워두었다면 그 계획은 동작하지 않는다.**
대신 **StatefulSet 의 롤링 업데이트가 사고를 절반에서 멈춰줬다.**
---
## 1. 전제 — 먼저 백업한다
D-1 에서 확인한 절차 그대로.
```bash
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
--clean --if-exists > /tmp/pre-upgrade.sql
```
```
백업: 396333 bytes
현재 이미지: quay.io/keycloak/keycloak:26.7.0
총 마이그레이션 수: 210
현재 세션: 4
```
### 개념 — `databasechangelog`
Keycloak 은 **Liquibase** 로 스키마를 관리한다. 적용한 변경 하나하나를
`databasechangelog` 테이블에 기록한다.
| 컬럼 | 뜻 |
|---|---|
| `id` / `author` / `filename` | 변경을 식별 |
| **`md5sum`** | **그 변경 정의의 체크섬** |
| `orderexecuted` | 적용 순서 |
**210개가 쌓여 있다.** 이것이 "이 DB 는 어느 버전까지 올라갔는가"의 기록이다.
---
## 2. 롤백을 시도했다 — 26.7.0 → 26.0
```bash
kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.0
```
```
+20초 keycloak-0:Running(1/1) keycloak-1:Running(0/1)
+80초 keycloak-0:Running(1/1) keycloak-1:Error(0/1)
+140초 keycloak-0:Running(1/1) keycloak-1:CrashLoopBackOff(0/1)
```
```
ERROR: Failed to start server in (production) mode
ERROR: liquibase.exception.ValidationFailedException: Validation Failed:
1 changesets check sum
```
### 왜 실패하는가 — 체크섬 불일치
```
26.7.0 이 적용한 변경 → databasechangelog 에 md5sum 기록
26.0 이 기동하며 검증 → 자기가 아는 그 변경의 md5sum 과 비교
└─ 다르다 → ValidationFailedException
```
**"모르는 변경이 있다" 가 아니라 "아는 변경인데 정의가 다르다" 이다.**
같은 changeset 이 버전 사이에 수정된 것이며, **더 엄격한 실패**다.
> **Liquibase 는 안전을 위해 기동 자체를 거부한다.**
> 스키마를 반쯤 아는 상태로 서비스하느니 안 뜨는 쪽을 고른 설계다.
---
## 3. 그런데 서비스는 살아 있었다
```
https://auth.hyeonworks.com/realms/master HTTP 200
ready 주소: [10.42.1.140] ← 한 파드만
statefulset desired/ready/updated: 2 / 1 / 1
```
**StatefulSet 의 롤링 업데이트가 한 번에 하나씩 바꾸기 때문**이다.
```
keycloak-1 을 26.0 으로 → 기동 실패 → Ready 가 안 됨
└─ StatefulSet 은 keycloak-0 을 건드리지 않는다
└─ keycloak-0 (26.7.0) 이 계속 서비스한다
```
**A-8 에서 "무중단은 replica ≥ 2 와 readiness 의 조합" 이라고 썼는데,
여기서는 그 조합이 잘못된 배포를 절반에서 멈춰줬다.**
| replica 1 이었다면 | |
|---|---|
| 유일한 파드가 CrashLoopBackOff | **전면 장애** |
| 되돌리려면 사람이 개입 | 그동안 계속 다운 |
---
## 4. 앞으로 되돌리기
```bash
kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.7.0
```
```
partitioned roll out complete: 2 new pods have been updated...
keycloak-0 1/1 Running
keycloak-1 1/1 Running 28s
realms|clients|migrations|sessions = 2|15|210|4
외부 진입점 HTTP 200
```
**정상 복구.** 마이그레이션 수도 세션도 그대로다 — **실패한 기동은 스키마를
건드리지 못했다.** Liquibase 가 검증 단계에서 멈췄기 때문이다.
---
## 5. 그래서 업그레이드 계획은 어떻게 세워야 하는가
```
✘ "문제가 생기면 이미지 태그를 되돌린다"
└─ 스키마가 이미 바뀌었으면 옛 버전이 안 뜬다
✔ "문제가 생기면 백업에서 DB 를 되돌리고 이미지도 되돌린다"
└─ D-1 에서 확인한 절차가 여기서 필요하다
```
| 단계 | |
|---|---|
| 1 | **백업** (D-1) — 이것이 유일한 되돌리기 수단이다 |
| 2 | 이미지 태그 변경 |
| 3 | **첫 파드만 관찰** — StatefulSet 이 멈춰준다 |
| 4 | 실패하면 **이미지를 되돌린다** (스키마가 안 바뀌었으면 이것으로 충분) |
| 5 | 스키마가 이미 바뀌었으면 **DB 도 복구**해야 한다 |
**4와 5를 가르는 것이 "Liquibase 가 검증에서 멈췄는가, 이미 적용했는가" 다.**
이번에는 검증에서 멈춰 4로 끝났다.
---
## 6. 이 실험이 확인한 것과 못 한 것
| | |
|---|---|
| ✔ 롤백이 안 된다는 것 | 체크섬 불일치로 기동 거부 |
| ✔ 실패가 안전하게 격리된다 | StatefulSet + readiness |
| ✔ 실패한 기동은 스키마를 안 건드린다 | 마이그레이션 210 그대로 |
| ✘ **정방향 업그레이드** | **26.7.0 보다 새 이미지가 없어 시험하지 못했다** |
| ✘ 마이그레이션 중 장애 | 스키마 변경 도중 죽으면? |
| ✘ 대규모 마이그레이션 시간 | 데이터가 작아 순식간이다 |
> **정방향을 시험하지 못한 것을 감춰서는 안 된다.**
> 다만 **역방향이 더 위험한 방향**이고, 그것이 실패한다는 사실이
> "롤백 계획" 을 무효로 만든다는 점에서 실무적으로 더 중요한 결과다.
>
> 새 버전이 나오면 같은 절차(백업 → 태그 변경 → 첫 파드 관찰)로 반복한다.
---
## 7. 재현 절차 (명령어)
```bash
# 1. 백업 먼저 (D-1)
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
--clean --if-exists > pre-upgrade.sql
# 2. 현재 마이그레이션 수를 기록
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
"select count(*) from databasechangelog"
# 3. 버전 변경
kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:<tag>
# 4. ★ 첫 파드만 본다. 실패하면 StatefulSet 이 멈춘다
kubectl -n keycloak-lab get pods -w
kubectl -n keycloak-lab logs keycloak-1 | grep -iE "liquibase|changeset|validation"
# 5. 서비스가 살아 있는지 (남은 파드가 받는다)
kubectl -n keycloak-lab get endpoints keycloak -o jsonpath='{.subsets[*].addresses[*].ip}'
# 6. 되돌리기 — 스키마가 안 바뀌었으면 이미지만으로 충분
kubectl -n keycloak-lab set image statefulset/keycloak keycloak=quay.io/keycloak/keycloak:26.7.0
```
---
## 8. 다음에 남기는 것
| | |
|---|---|
| **D-3** 비밀 관리 | 업그레이드 시 Secret 도 같이 검토된다 |
| 운영 | **롤백 = 백업 복구**다. 태그만 되돌리는 계획은 반쪽이다 |
| 운영 | **replica ≥ 2** 가 잘못된 배포를 절반에서 멈춘다 |
| 미검증 | 정방향 업그레이드, 마이그레이션 중 장애, 대규모 소요 시간 |