diff --git a/docs/evidence/d2-version-upgrade/01-pre-upgrade.txt b/docs/evidence/d2-version-upgrade/01-pre-upgrade.txt new file mode 100644 index 0000000..14e57dd --- /dev/null +++ b/docs/evidence/d2-version-upgrade/01-pre-upgrade.txt @@ -0,0 +1,9 @@ +=== D-1 의 교훈: 업그레이드 전에 백업한다 === + 백업: 396333 bytes + +=== 현재 버전과 스키마 상태 === +quay.io/keycloak/keycloak:26.7.0 + 총 마이그레이션 수: 210 + +=== 로그인 상태 만들기 (업그레이드 후 살아남는지 볼 것) === + 현재 세션: 4 diff --git a/docs/evidence/d2-version-upgrade/02-rollback-attempt.txt b/docs/evidence/d2-version-upgrade/02-rollback-attempt.txt new file mode 100644 index 0000000..bcc29eb --- /dev/null +++ b/docs/evidence/d2-version-upgrade/02-rollback-attempt.txt @@ -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 diff --git a/docs/evidence/d2-version-upgrade/03-roll-forward.txt b/docs/evidence/d2-version-upgrade/03-roll-forward.txt new file mode 100644 index 0000000..0991241 --- /dev/null +++ b/docs/evidence/d2-version-upgrade/03-roll-forward.txt @@ -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 diff --git a/docs/evidence/d2-version-upgrade/README.md b/docs/evidence/d2-version-upgrade/README.md new file mode 100644 index 0000000..bd752af --- /dev/null +++ b/docs/evidence/d2-version-upgrade/README.md @@ -0,0 +1,16 @@ +# D-2 — 버전 업그레이드 증거 + +2026-09-04 17:05–17: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)가 필요하다. diff --git a/docs/experiment-d2-version-upgrade.md b/docs/experiment-d2-version-upgrade.md new file mode 100644 index 0000000..5cec053 --- /dev/null +++ b/docs/experiment-d2-version-upgrade.md @@ -0,0 +1,213 @@ +# D-2 — 버전을 올리고 내릴 때 무엇이 일어나는가 + +브랜치 `feature/keycloak-d2-version-upgrade` · +증거 [`docs/evidence/d2-version-upgrade/`](evidence/d2-version-upgrade/) · +2026-09-04 17:05–17: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: + +# 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** 가 잘못된 배포를 절반에서 멈춘다 | +| 미검증 | 정방향 업그레이드, 마이그레이션 중 장애, 대규모 소요 시간 |