# 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** 가 잘못된 배포를 절반에서 멈춘다 | | 미검증 | 정방향 업그레이드, 마이그레이션 중 장애, 대규모 소요 시간 |