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>
30 KiB
D-2 재현 가이드 — 태그를 올리고 내려 보고 롤백이 되는 조건을 직접 본다
해설 문서: docs/experiment-d2-version-upgrade.md ·
증거 원문: docs/evidence/d2-version-upgrade/ ·
docs/evidence/followup/
이 가이드가 끝나면
당신 터미널에서 이것들을 직접 본다.
| 보게 되는 것 | 어디서 |
|---|---|
업그레이드 전후로 databasechangelog 행 수가 그대로인 것 |
psql -tAc |
파드가 하나씩 갈리는 동안 정문이 계속 200 인 것 |
1초 폴링 |
| 같은 스키마에서는 롤백이 되는 것 | 태그를 되돌리고 다시 폴링 |
전환 순간의 000 이 서버 오류가 아닌 것 |
--max-time |
스키마가 바뀐 방향에서 ValidationFailedException 으로 기동이 거부되는 것 |
logs keycloak-1 |
| 그때도 서비스가 살아 있는 것 — StatefulSet 이 절반에서 멈춘다 | endpointslice |
| 실패한 기동이 스키마를 안 건드린 것 | 다시 databasechangelog |
전제
D-1이 끝나 있고 덤프가 손에 있다. 이 실험의 되돌리기 수단은 태그가 아니라 그 파일일 수 있다.A-8— 롤링 재시작이 무중단이라는 것이 전제다.- 명령은
kc-lab-1에서 친다.kubectl은sudo로 쓴다. - 네임스페이스는
keycloak-lab이다. jq는 이 실험대 어디에도 없다. 이 가이드는jq를 쓰지 않는다.- 터미널 두 개를 열어 두면 편하다. 하나는 가용성 폴링용, 하나는 관찰용.
주의 — 이건 실제로 버전을 바꾸는 실험이다
이미지 태그를 세 번 바꾼다(정방향 → 롤백 → 그리고 선택적으로 실패하는
방향). 마지막 것은 파드를 CrashLoopBackOff 로 만든다. 되돌리는 명령은
각 절 첫머리에 있고, 전부 태그 한 줄이다. 전 구간 약 20분이며, 중간에
그만두려면 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 의 절차 그대로
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
(첫 실행) · followup/01(후속 실행)
백업: 396333 bytes
백업: 395375 bytes
어디를 봐야 하는가 — 크기, 그리고 tail 의 dump complete.
이 파일이 없으면 이 실험을 하지 않는다. 5절에서 왜인지 나온다.
1-2. 지금 무엇이 돌고 있나
확인
sudo kubectl -n keycloak-lab get statefulset keycloak \
-o jsonpath='{.spec.template.spec.containers[0].image}'; echo
실측 — 01-pre-upgrade.txt
quay.io/keycloak/keycloak:26.7.0
어디를 봐야 하는가 — 태그. latest 로 되어 있으면 이 실험이 성립하지
않는다 — 무엇에서 무엇으로 가는지 말할 수 없기 때문이다.
StatefulSet 에 적힌 것과 파드가 실제로 돌리고 있는 것은 다를 수 있다 (적용 중이거나, 롤아웃이 멈춰 있으면).
확인
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. ★ 마이그레이션 수 — 이 숫자가 이 실험의 전부다
확인
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select count(*) from databasechangelog"
형태
count
-------
210
(1 row)
비교용으로 값만 뽑는 형태도 익혀 둔다.
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
"select count(*) from databasechangelog"
실측 — 두 실행 모두
총 마이그레이션 수: 210
어디를 봐야 하는가 — 숫자 하나. 이 값을 화면 밖에 적어 둔다.
무엇이 마지막으로 적용됐는지도 한 번 본다. 나중에 「스키마가 언제 움직였나」를 물을 때 여기를 본다.
확인 — 미검증(원래 실행은 개수만 셌다)
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. 세션 — 업그레이드가 로그인 상태를 날리는지 본다
확인
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 판까지 적어 둔다
확인
sudo kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
실측 — followup/01 의
업그레이드 후 값
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. 새 태그가 실제로 있는지 확인한다
확인 — 레지스트리에 무엇이 있나. 처음 한 번은 그대로 본다
curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyActiveTags=true"
한 줄짜리 JSON 이 통째로 나온다. 어떤 필드가 있는지 보고 나서 자른다.
미검증 — jq 가 없으므로 이 실험대에서는 이렇게 읽는다
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회, 뒤에서 돌린다
( 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 ) &
되돌리기 — 그만 재려면
kill %1
확인 — 30초쯤 두고 먼저 평시를 센다
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)
되돌리기를 먼저 읽는다.
sudo kubectl -n keycloak-lab set image statefulset/keycloak \
keycloak=quay.io/keycloak/keycloak:26.7.0
단, 이 되돌리기가 유효한 것은 databasechangelog 가 안 바뀌었을 때뿐이다.
바뀌었으면 되돌리기는 「덤프 복구 + 태그 되돌리기」다(5절).
하기
date '+%H:%M:%S 태그 변경'
sudo kubectl -n keycloak-lab set image statefulset/keycloak \
keycloak=quay.io/keycloak/keycloak:26.7.3
실측 — followup/01
시작: 15:22:59
statefulset.apps/keycloak image updated
어디를 봐야 하는가 — image updated 한 줄. 이건 「적용됐다」가 아니라
「접수됐다」다. 실제 교체는 지금부터 일어난다.
하기 — 끝날 때까지 블록한다
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. 파드가 새 이미지로 돌고 있나
확인
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
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 가 올라가 있으면 새 파드가 기동에 실패해 재시작을 반복하는 것이다.
확인 — 실제로 새 파드인지는 나이로 본다
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. 버전이 정말 바뀌었나 — 파드가 자기 입으로 말하게 한다
확인
sudo kubectl -n keycloak-lab logs keycloak-0 | grep -i 'Keycloak 26' | tail -1
실측
Keycloak 26.7.3
어디를 봐야 하는가 — 로그가 말하는 판. 이미지 태그와 다르면 태그가 재사용된 것이다(같은 태그가 다른 내용을 가리키는 경우).
4. 효과를 관찰한다
4-1. 끊겼나
확인
tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
실측 — followup/01
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.
무엇이 보이나 — cluster_size 가 2 → 1 → 2 를 두 번 반복하고, 파드별
up 시계열이 끝나고 새 시계열이 시작된다.
어디를 봐야 하는가 — 2 → 1 → 2 가 두 번인 것. 파드가 둘이므로 교체도 두 번이고, 그때마다 클러스터가 잠시 한 명이 된다. 한 번만 보이면 두 파드가 동시에 갈린 것이다.
4-3. ★ 스키마가 움직였나 — 이 실험의 판정
확인 — 1-3 과 똑같은 명령
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
"select count(*) from databasechangelog"
실측 — followup/01
마이그레이션 후: 210 (전: 210)
세션 후: 3 (전: 3)
어디를 봐야 하는가 — 전과 후가 같은가.
이 결과가 의미하는 것
| 결과 | 뜻 | 되돌리는 법 |
|---|---|---|
| 행 수가 그대로 | 스키마가 안 움직였다 | 태그만 되돌리면 된다 |
| 행 수가 늘었다 | 새 changeset 이 적용됐다 | 덤프 복구 + 태그 되돌리기 |
26.7.0 → 26.7.3 은 패치 릴리스라 스키마가 그대로였다. 그래서 롤백이 가능하다는 가설이 섰고, 다음 절에서 시험한다.
4-4. 가설 시험 — 같은 스키마에서 롤백해 본다
되돌리기 — 이 절 자체가 되돌리기다. 다시 올리려면 태그를 26.7.3 으로.
하기
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
시작: 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 한 번을 오해하지 않는다
확인
tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
grep -n '000' /tmp/d2-avail.txt
실측 — followup/02
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절까지로 이 실험의 판정은 끝났다. 이 절은 「행 수가 바뀌었을 때」가 실제로 어떤 모양인지 보려는 것이다.
되돌리기 — 먼저 읽는다
sudo kubectl -n keycloak-lab set image statefulset/keycloak \
keycloak=quay.io/keycloak/keycloak:26.7.0
하기
date '+%H:%M:%S 26.0 으로 내린다'
sudo kubectl -n keycloak-lab set image statefulset/keycloak \
keycloak=quay.io/keycloak/keycloak:26.0
확인 — 이번에는 rollout status 로 기다리지 말고 눈으로 본다
sudo kubectl -n keycloak-lab get pods -w
시각: 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. 왜 실패했는지 물어본다
확인
sudo kubectl -n keycloak-lab logs keycloak-1 | grep -iE 'liquibase|changeset|validation'
실측 — 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 는 스키마를 반쯤 아는 상태로 서비스하느니 기동 자체를 거부한다.
파드가 이미 죽어서 로그가 안 나오면 직전 컨테이너의 로그를 본다.
sudo kubectl -n keycloak-lab logs keycloak-1 --previous
5-2. 그런데 서비스는 살아 있다
확인
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
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. 실패한 기동이 스키마를 건드렸나
확인 — 세 번째로 같은 명령
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. 복구
하기
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
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 |
업그레이드 전 행 수를 안 적었을 때 — 덤프 안에 그 테이블이 통째로 들어 있다. 미검증
sed -n '/^COPY public.databasechangelog /,/^\\\.$/p' /tmp/pre-upgrade.sql | wc -l
어디를 봐야 하는가 — 나온 수에서 2를 뺀다(COPY 줄과 \. 줄).
이게 백업 시점의 행 수다. D-1 의 덤프가 여기서 한 번 더 값을 한다.
다음
| 실험 | D-2 가 남긴 것 |
|---|---|
| D-1 백업·복구 | 롤백 = 백업 복구인 경우가 있다. 태그만 되돌리는 계획은 반쪽이다 |
| D-3 비밀 관리 | 업그레이드할 때 Secret 도 같이 검토된다 |
| A-8 롤링 재시작 | replica ≥ 2 가 잘못된 배포를 절반에서 멈춘다 |
| 운영 | 판정은 버전 번호가 아니라 databasechangelog 의 행 수다 |
| 전부 | 재기 전에 못 재는 값을 먼저 적어 둔다. 업그레이드 전 행 수가 그것이다 |