기반 가이드 7단계로 실험대를 철거하고 다시 세운 뒤 virtualization setup 9편과 keycloak-session-store 26편을 순서대로 밟았다. 24편은 끝까지, 11편은 되는 데까지 밟았고 밟은 범위를 편마다 적었다. 명령이 못 도는 것을 고쳤다. - kubectl 을 `kc-lab-1` 에서 치라고 적었는데 그 기계에 kubeconfig 가 없다. 라벨 639개와 각 편의 「어디서 치는가」를 `[lab host]` 로 옮겼다 - `-o custom-columns=…[0]…` 이 zsh 에서 글로브로 읽혀 안 돈다. 28곳에 따옴표 - busybox `sed` 가 끝 개행을 안 붙여 A-3 의 측정이 언제나 0 이었다 - `--token-file ~/node-token` 뒤에 그 파일을 지우면 k3s agent 가 재부팅을 못 견딘다. `/etc/rancher/node-token` 으로 옮기는 처방을 재서 넣었다 - 게스트에 없는 도구를 전제로 한 명령 넷 — `conntrack`·`dig`·`strings`·`nginx -v` - `echo` 와 JWT 헤더가 `"이름" : [ 값 ]` 으로 찍는데 문서는 공백 없이 옮겨 적어 그 실측으로 만든 grep·sed 가 한 줄도 못 잡는다 - B-0 이 `directAccessGrantsEnabled` 와 계정 완성을 빠뜨려 B-3 이 못 돈다 - D-4·D-4a 가 `test-server` 와 `certbot-renew.*` 를 가리키는데 실제로는 `kc-lab-edge` 의 `certbot.service` 다 - `virsh setmaxmem --config` 를 `dominfo` 로 판정하면 틀린다. `--inactive` 로 - `LIBVIRT_DEFAULT_URI` 를 rc 에만 넣으면 `ssh host '명령'` 에서 안 먹는다 결과가 조건부인 것을 갈랐다. - readiness 는 즉시 안 뒤집힌다. A-1·A-2 의 60초 창을 적었다 - 03 의 층 ②③ `301` 은 04 이후의 값이고 그 단계에서는 `404` 다 - A-0 의 로그 필터를 요청 직후에 치면 정반대 결론이 나온다 - A-5 의 한 방향 차단은 잠깐 `1` 이었다 `2` 로 돌아온다 증거는 두 프로젝트의 `evidence/raw/` 에 99벌을 README 와 함께 남겼다. 비밀은 길이만 적었고 화면에 찍힌 토큰은 가렸다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
45 KiB
id, kind, slug, title, topic, topicName, project, status, studio, pinnedVersions, source, sourceRevision
| id | kind | slug | title | topic | topicName | project | status | studio | pinnedVersions | source | sourceRevision | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 7dc48b91-e31b-455c-9a9d-c766f95ff491 | SETUP | reproduce-d1-backup-restore | 스키마를 통째로 지우고 덤프 하나로 되살아나는지 본다 | operations-that-report-success | 운영 절차의 완료 판정 — 백업 · 판올림 · Secret · 인증서 갱신 | keycloak-session-store | 게시 전 | https://hyeonworks.com/studio/documents/7dc48b91-e31b-455c-9a9d-c766f95ff491/edit |
|
|
cdac9b8178391311d8eca1ebc6cac15bb62d79af |
스키마를 통째로 지우고 덤프 하나로 되살아나는지 본다
덤프를 뜨고 검증 넷을 통과한 뒤 스키마를 통째로 지우고 같은 명령으로 복구를 대조하는 절차다. 되돌리는 수단이 방금 뜬 파일 하나뿐이라 검증이 파괴보다 먼저 온다.
관계
- 되돌리기를 막은 것은 체크섬이었고 그 판정은 조건부였다 이 절차가 남긴 덤프 위에 서는 판올림 실험의 결론이다. 스키마가 움직인 뒤에는 덤프만이 되돌리는 수단이 된다.
- 주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다
-i를 빠뜨린 복구가 그 아홉 건과 같은 모양으로 끝난다. 왜 따로 확인해야 하는지를 그쪽이 적는다. - up 지표는 살아 있지만 쓸모없는 상태를 보지 못한다
데이터베이스가 통째로 비었는데 정문이
200인 상태를 지표가 어떻게 놓치는지 다룬다. - 이미지 태그를 올렸다 내리며 롤백이 언제 막히는지 가른다 이 절차 다음에 오는 편이고, 여기서 뜬 덤프를 전제로 시작한다. 덤프를 지우지 않는 까닭이 그쪽에 있다.
- PostgreSQL 을 정상 종료시키고 네 경로를 잰다
프로세스가 죽었을 때의 모양이다. 먼저 봐 둬야 여기의
200이 얼마나 이상한지 안다. - PostgreSQL 을 진짜로 크래시시키고 잃은 로그인을 센다 실제 복구 지점 목표의 두 번째 겹이 그 편에서 나온다.
본문
읽기 전에 — 어디서 치는가
명령은 [lab host] 에서 친다. kubectl 에 sudo 를 붙이지 않는다 — 가이드가 「kubeconfig 를 사용자 홈에 복사해 뒀다면 sudo 는 빼도 된다」고 스스로 괄호를 달아 두었다. 마지막 한 단계만 호스트(test-server)로 넘어가고, 거기서는 사람이 비밀번호를 친다.
[kc-lab-1] 라벨이 붙은 블록은 게스트 셸이다. lab host 에서 ssh kc-lab-1 로 들어가서 치고, 끝나면 exit 로 나온다. ssh kc-lab-2 '…' 한 줄 형태는 lab host 에서 그대로 쳐도 되므로 [lab host] 로 두었다.
원 가이드는 이 명령들을 kc-lab-1 에서 치라고 적었다. 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 sudo 없는 kubectl 이 permission denied 로 막힌다 — kubeconfig 는 lab host 의 ~/.kube/config 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 kubectl 블록의 기계 이름을 [lab host] 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
| 무엇 | 값 |
|---|---|
| 네임스페이스 | keycloak-lab |
| 주입 수단 | DROP SCHEMA public CASCADE; CREATE SCHEMA public; |
| 되돌리는 수단 | 방금 뜬 덤프 파일 하나 — /tmp/keycloak-backup.sql |
| 전 구간 | 약 20분. 파괴 구간 자체는 1분 안쪽 |
| 잃는 것 | realm · client · user · 세션 전부 |
jq |
이 실험대 어디에도 없다. 이 절차도 쓰지 않는다 |
이 절차에는 스크립트가 없다. 원래 실행은 백업·파괴·복구를 스크립트 하나로 돌렸고, 그래서 증거 파일의 줄에는 realms|clients|users|sessions|authclients = 2|15|2|3|1 처럼 이름표가 붙어 있다. 사람이 치는 형태가 아니다. 그리고 이 실험에서 스크립트는 특히 위험하다 — DROP SCHEMA 와 복구가 한 파일에 있으면 중간에서 멈췄을 때 무엇이 실행됐는지 알 수 없다. 파괴를 손으로 치고, 눈으로 확인하고, 복구도 손으로 친다.
이 실험이 가르는 것
「백업이 있다」와 「복구해 봤다」는 다른 문장이다. 백업 스크립트가 매일 도는 것과 그 파일로 실제로 서비스를 되살리는 것 사이에는 시험되지 않은 가정이 여러 개 있고, 이 절차는 그중 둘을 판정한다.
| # | 질문 | 어떻게 가르나 |
|---|---|---|
| ① | 덤프에 필요한 것이 다 들어가는가 | 특히 세션. 안 들어가면 복구 후 전원 재로그인이다 |
| ② | 복구 절차가 실제로 도는가 | 오류 없이 끝나고 데이터가 일치하는가 |
부수 질문이 하나 붙는다 — 데이터베이스가 비면 무엇이 깨지는가. 프로세스를 내린 A-2 와 여기가 갈라진다.
A-2 DB 프로세스 정지 → 커넥션 실패 → readiness DOWN → 파드가 Service 에서 빠짐
D-1 스키마만 삭제 → 커넥션 정상 → readiness UP → ?
커넥션은 되는데 테이블이 없는 상태는 단일 장애 주입으로 잘 안 만들어진다. 그래서 이 절차가 따로 있다.
가이드는 끝났을 때 확인되는 것을 일곱으로 적는다. 덤프 파일 안에 세션 행이 실제로 들어 있는 것, 데이터베이스를 통째로 비웠는데 정문이 200 인 것, 파드가 1/1 Running 인 채로 테이블이 0개인 것, certs 200 · well-known 500 · 토큰 400 으로 부분만 깨지는 것, 복구가 1초 만에 오류 0건으로 끝나는 것, 세션까지 되살아나는 것, 그리고 덤프가 데이터베이스와 같은 기계 위에 놓여 있는 것.
복구가 이 편에서는 관찰의 일부다. 질문 ②의 답이 복구 절에서 나오므로 아래 「복구와 원상복구 확인표」는 원상복구만이 아니라 판정을 함께 싣는다.
전제와 되돌리기
- A-2 를 먼저 하면 좋다. 데이터베이스 프로세스가 죽었을 때의 모양을 봐 둬야 이 실험의
200이 얼마나 이상한지 안다. - A-3 도 먼저다. 실제 복구 지점 목표의 두 번째 겹이 거기서 나온다.
- 네임스페이스는
keycloak-lab이다. - 덤프를 다른 기계로 옮기는 마지막 단계만 호스트(
test-server)가 필요하고, 호스트의sudo는 비밀번호를 묻는다. 그 부분은 사람이 직접 친다.
이 실험은 데이터베이스를 비운다. DROP SCHEMA public CASCADE 는 realm·client·user·세션을 전부 지운다. 되돌리는 수단은 방금 뜬 덤프 파일 하나뿐이고, 그래서 덤프를 검증하기 전에는 주입 절로 넘어가지 않는다.
되돌리기는 한 줄이고, 파괴하기 전에 읽어 둔다.
kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak \
< /tmp/keycloak-backup.sql
-i 가 이 명령의 전부다. 빠뜨리면 아무 일도 안 일어나고 오류도 안 난다. 왜 그런지는 주입 검증 절의 마지막 단계에서 본다.
주입 전에 같은 명령으로 먼저 본다
시험군만 재는 측정은 측정이 아니다. 파괴 후에 볼 것을 파괴 전에 똑같은 명령으로 먼저 봐 둔다. 복구가 완전 일치인지 판정하려면 일치시킬 상대가 있어야 하는데, DROP SCHEMA 를 친 뒤에는 그 상대를 만들 방법이 없다. 넓은 것부터 좁혀 가고, 마지막 세 칸은 덤프 자체를 향한다.
파드 → 데이터 개수 → 세션 → 밖에서 본 상태 → 덤프 → ★ 덤프 검증 → 덤프의 위치
1. 파드가 어디에 몇 개 있는가
무엇을 보는가 — 파드 넷의 상태와 배치.
kubectl -n keycloak-lab get pods -o wide
어디를 보나 — 실측은 이렇다(observed, 02-destruction.txt). 파괴 직후 목록인데 파괴 전후가 같다는 것이 이 실험의 결과이므로 파괴 전 값으로도 읽는다. 증거에 옮겨진 네 줄에는 -o wide 가 덧붙이는 NODE 열이 없고 postgres 행도 빠져 있다. 아래에 없다고 해서 그 파드가 없지는 않다 — 노드 이름과 postgres 행은 자기 화면에서 읽는다.
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
이 값이 뜻하는 것 — READY 가 전부 1/1 이고 RESTARTS 가 0 이다. 자기 화면의 NODE 열에서 postgres 파드가 어느 노드에 떠 있는지 읽고 적어 둔다 — 덤프가 그 노드와 같은 디스크에 놓였는지를 §7 에서 그 이름으로 가른다. 뒤에서 RESTARTS 가 오르면 파괴가 엉뚱한 데를 건드렸다는 신호다.
2. 데이터가 몇 건 있는가
무엇을 보는가 — realm·client·user·세션의 개수. 처음 한 번은 읽는 형태로 친다. 값만 뽑는 형태부터 배우면 psql 이 무엇을 돌려주는지 모르게 된다.
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select count(*) from realm"
어디를 보나 — 모양은 이렇다(observed).
count
-------
2
(1 row)
숫자 하나와 (1 row) 를 본다. 여기서 오류가 나면 뒤의 모든 단계가 무의미하다. psql: error: connection to server ... failed 면 데이터베이스가 아직 안 붙은 것이고, relation "realm" does not exist 면 스키마가 이미 없다.
이제 넷을 한 줄로 모은다. 비교할 값이 필요할 때만 이 형태를 쓴다.
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')"
어디를 보나 — 실측은 이렇다(observed, 01-backup.txt).
realms|clients|users|sessions|authclients = 2|15|2|3|1
이 값이 뜻하는 것 — 원래 실행은 스크립트로 돌렸고 인가된 클라이언트를 하나 더 셌다. 그래서 증거 줄에는 값이 다섯이고 이름표가 붙어 있다. 위 명령으로 넷을 뽑으면 이름표 없이 2|15|2|3 만 나온다. 다섯째 쿼리는 해설 문서의 재현 절차에 남아 있지 않아 가이드가 넷으로 뒀다 — 없는 컬럼을 지어내지 않고, 다섯째가 필요하면 세는 쿼리를 정해서 양쪽에 같이 쓴다. -tAc 는 헤더 없이(-t) 정렬 없이(-A) 한 줄만이라는 뜻이다.
이 줄을 그대로 복사해 둔다. 복구 후에 같은 명령을 쳐서 문자 단위로 같은지 본다. 하나라도 다르면 복구가 부분적으로만 됐다.
3. 세션이 데이터베이스 안에 있는가
무엇을 보는가 — 질문 ①의 재료. 세션 행이 실제로 테이블에 있어야 덤프에 들어갈 것이 있다.
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select user_session_id, offline_flag, realm_id from offline_user_session"
어디를 보나 — 행이 몇 개 있고 id 가 어떻게 생겼는지만 본다(observed).
user_session_id | offline_flag | realm_id
--------------------------+--------------+--------------------------------------
E1q5xI7tt4U_WhZpW7rEPIF2 | 0 | 7845f394-723a-4d07-b530-c7416b2e1d31
...
이 값이 뜻하는 것 — 행이 0개면 질문 ①을 판정할 수 없다. 그때는 관리 콘솔에 한 번 로그인해서 세션을 만들고 다시 본다. 세션이 데이터베이스 테이블에 있다는 것은 persistent-user-sessions 가 켜져 있다는 뜻이고(A-0), 그래서 세션이 백업 대상이 된다. volatile 이었다면 세션은 애초에 데이터베이스에 없고 복구해도 전원 재로그인이라 백업의 값어치가 달라진다.
두 쿼리가 다른 것을 센다. 앞의 개수 쿼리는 offline_flag='0' 만 셌고 이 쿼리는 전부 나열한다. 원래 실행에서도 개수는 3, 나열은 4 rows 였다(03-restore.txt). 두 숫자가 다른 것을 이상하게 여기지 말고 복구 전후에 같은 쿼리끼리 비교한다.
문제가 생기면 — 나열은 되는데 개수가 0이면 offline_flag 필터를 의심한다.
4. 밖에서는 무엇이 보이는가
무엇을 보는가 — 정문과 app1 의 응답. 처음 한 번은 응답을 읽는다.
curl -I https://auth.hyeonworks.com/realms/master
헤더가 통째로 나온다. HTTP/2 200, content-type: application/json 을 본다. 같은 것을 반복해서 재고 비교할 때만 코드만 뽑는다.
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
curl -s -o /dev/null -w '%{http_code}\n' https://app1.hyeonworks.com/
어디를 보나 — 실측은 이렇다(observed, 02-destruction.txt). 이것도 파괴 직후 값이고, 그게 결과다.
https://auth.hyeonworks.com/realms/master HTTP 200
https://app1.hyeonworks.com/ HTTP 200
이 값이 뜻하는 것 — 지금은 당연히 200 이다. 파괴 뒤에도 같은 값이 나오므로 이 두 줄은 정상 판정에 쓸 수 없는 지표의 예로 남는다.
5. 덤프를 뜬다
목적 — 되돌리는 수단을 만든다. 이 파일 없이는 다음 절로 못 간다.
① 시각을 남기고 덤프를 뜬다.
date '+%H:%M:%S 백업 시작'
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
--clean --if-exists > /tmp/keycloak-backup.sql
date '+%H:%M:%S 백업 완료'
예상 결과 — 실측은 이렇다(observed, 01-backup.txt).
시작: 14:59:30
완료: 14:59:30
크기: 394945 bytes (6956 줄)
시각 두 줄과 파일 크기를 본다. 이 규모에서는 1초 미만이다.
왜 필요한가 — 두 옵션은 짝이다.
| 옵션 | 무엇을 하나 | 없으면 |
|---|---|---|
--clean |
복구 시 기존 객체를 DROP 하고 다시 만든다 | already exists 오류가 쏟아진다 |
--if-exists |
없는 객체를 DROP 할 때 오류를 안 낸다 | 깨끗한 데이터베이스에 복구할 때 오류가 쏟아진다 |
--clean 만 주면 빈 데이터베이스에 넣는 복구가 깨지고, --if-exists 만 주면 아무 효과가 없다 — DROP 문 자체가 안 만들어진다. 이 실험은 어차피 빈 데이터베이스에 복구하는데도 두 옵션이 필요하다. 실제 사고는 대개 그렇지 않고, 반쯤 남은 데이터베이스에 덤프를 밀어 넣는 상황이 훨씬 흔하며 그때 이 둘이 있고 없고가 갈린다.
문제가 생기면 — 이 단계는 읽기만 하므로 파일이 마음에 안 들면 지우고 다시 뜬다.
rm -f /tmp/keycloak-backup.sql
6. 덤프를 검증한다 — 넷을 통과해야 다음 절로 간다
목적 — 「파일이 생겼다」와 「복구할 수 있다」를 가른다. pg_dump 가 중간에 실패해도 파일은 남고 크기도 0 이 아니다. 이 단계를 건너뛰면 주입 절은 자살행위라고 가이드는 적는다.
① 크기와 줄 수.
ls -l /tmp/keycloak-backup.sql
wc -l /tmp/keycloak-backup.sql
예상 결과 — 실측은 이렇다(observed).
크기: 394945 bytes (6956 줄)
② 테이블 수.
grep -c '^CREATE TABLE' /tmp/keycloak-backup.sql
예상 결과 — 실측은 이렇다(observed).
포함된 테이블 수: 101
101 이라는 절대값이 아니라 앞에서 본 데이터베이스와 자릿수가 맞는지가 중요하다. 두 자리로 떨어지면 덤프가 잘렸다.
③ 끝까지 쓰였는가.
tail -3 /tmp/keycloak-backup.sql
예상 결과 — 모양은 이렇다(observed).
--
-- PostgreSQL database dump complete
--
dump complete 를 본다. 이 줄이 없으면 덤프가 중간에 끊긴 것이고 그 파일로는 복구가 안 된다. 한 줄이 「파일이 생겼다」와 「덤프가 끝났다」를 가른다.
④ 세션이 들어갔는가. 이것이 질문 ① 자체다.
grep -c 'offline_user_session' /tmp/keycloak-backup.sql
grep -A3 'COPY public.offline_user_session' /tmp/keycloak-backup.sql | cut -c1-110
예상 결과 — 실측은 이렇다(observed, 01-backup.txt).
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 1788501263 0 {"ipAddr
왜 필요한가 — COPY 줄 다음에 실제 데이터 행이 붙어 있는가를 본다. COPY ... FROM stdin; 바로 뒤에 \. 만 있으면 테이블 정의만 들어가고 행은 비어 있는 것이고, 그건 세션을 백업하지 못한 덤프다. cut -c1-110 은 data 열의 JSON 이 화면을 뒤덮는 것을 막으려는 것이라, 처음 한 번은 cut 없이 쳐서 한 행이 얼마나 긴지 봐 둔다.
세션은 덤프에 들어간다. 질문 ①의 답은 「들어간다」이고 근거가 이 COPY 블록이다. 복구 절에서 같은 id 들이 되살아나는 것을 확인한다.
문제가 생기면 — 넷 중 하나라도 어긋나면 덤프를 지우고 다시 뜬다. 다음 절로 넘어가지 않는다.
7. 덤프가 지금 어디에 있는가
무엇을 보는가 — 파일의 경로와 그 파일이 올라앉은 디스크.
ls -l /tmp/keycloak-backup.sql
df -h /tmp
이 값이 뜻하는 것 — 경로가 /tmp 다. 이 파일은 지금 kubectl 을 친 그 기계의 디스크에 있다. A-4 에서 local-path PVC 가 노드에 못박혀 있는 것을 봤고, 그 노드가 안 돌아오면 데이터베이스 볼륨도 안 돌아온다. 그때 유일한 길이 덤프인데 덤프도 같은 기계에 있으면 같이 사라진다. 같은 장애 도메인에 있는 백업은 백업이 아니다. 원래 실행에서도 덤프는 test-server:/tmp 에 있었고, 해설 문서는 그것을 가장 중요한 미검증 항목으로 기록했다. 옮기는 절차는 복구 절에 있고, 파괴 전에는 읽어만 두고 실제 이동은 복구가 끝난 뒤에 한다.
주입
여기부터 데이터가 사라진다. 되돌리는 명령은 전제 절에 있고, 덤프 검증 넷을 통과하지 않았으면 지금 돌아가서 한다.
1. 스키마를 통째로 지운다
목적 — 커넥션은 살아 있는데 테이블만 없는 상태를 만든다.
① 시각을 남기고 친다.
date '+%H:%M:%S 파괴'
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
예상 결과 — 실측은 이렇다(observed, 02-destruction.txt).
=== ★ 파괴 — 스키마를 통째로 지운다 ===
시각: 14:59:47
DROP SCHEMA
CREATE SCHEMA
DROP SCHEMA 와 CREATE SCHEMA 두 줄을 본다. NOTICE: drop cascades to 101 other objects 같은 줄이 함께 나오는 것이 정상이다.
왜 필요한가 — 시각을 반드시 적어 둔다. 복구 절의 복구 시간 목표가 이 시각에서 시작한다. CREATE SCHEMA public 을 붙이는 까닭은 public 스키마 자체를 지우면 복구 스크립트가 들어갈 곳이 없기 때문이다. 지우는 것은 안의 객체이고, 빈 스키마는 남겨 둬야 pg_dump 출력이 그대로 들어간다.
문제가 생기면 — -d 인자를 본다. 다른 데이터베이스에 걸렸으면 다음 절의 테이블 수가 101 그대로 나온다.
주입 검증
결과를 해석하기 전에, 의도한 것만 지워졌는지 먼저 본다.
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select count(*) from pg_tables where schemaname='public'"
이 실험대는 스크립트로 셌고(observed), 위 형태도 2026-09-17 에 쳐서 0 을 받았다(observed). 결과는 이렇다(observed).
남은 테이블: 0
0 이어야 한다. 여기서 101 이 그대로 나오면 DROP 이 다른 데이터베이스에 걸린 것이고 -d 인자를 본다.
애플리케이션 테이블이 정말 없는지 직접 물어본다.
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select count(*) from realm"
모양은 이렇다(observed).
ERROR: relation "realm" does not exist
LINE 1: select count(*) from realm
^
커넥션은 성립하고 SQL 도 파싱된다. 테이블만 없다. 이 구별이 이 실험의 전부다. A-2 에서는 여기가 connection to server ... failed 였다.
그런데 밖은 멀쩡하다.
kubectl -n keycloak-lab get pods -o wide
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
curl -s -o /dev/null -w '%{http_code}\n' https://app1.hyeonworks.com/
실측은 이렇다(observed, 02-destruction.txt).
https://auth.hyeonworks.com/realms/master HTTP 200
https://app1.hyeonworks.com/ HTTP 200
keycloak-0 1/1 Running 0 4m15s
keycloak-1 1/1 Running 0 4m38s
1/1, RESTARTS 0, 그리고 200 이다. 데이터베이스가 통째로 비었는데 정문이 200 이다. 여기서 파괴가 실패했다고 읽으면 틀린다 — 테이블이 0개인 것을 바로 앞에서 봤다. 파괴는 성공했고 관측 지점이 그것을 못 본다. Keycloak 이 realm 정보를 Infinispan realms 캐시에서 서빙하기 때문이고(A-0 에서 그 캐시에 57개 엔트리가 있는 것을 봤다), 캐시는 읽을 때 데이터베이스와 대조하지 않는다. A-1 에서 로그아웃한 세션이 반대편에서 200 을 받았던 것과 같은 성질이다.
엉뚱한 것을 죽이지 않았는지도 본다.
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"
ready 주소가 여전히 둘이다. 아무 파드도 Service 에서 빠지지 않았다. A-2 에서는 여기가 빈 목록이었다. kubectl get endpoints 는 v1.33+ 에서 deprecated 이고, 이 실험대에서 실제로 그 경고를 봤다.
이 층의 조용한 실패는 복구 쪽에서 온다
여기까지의 주입 검증은 쉽게 통과한다. 어려운 확인은 반대편에 있다. 복구 명령에서 -i 를 빠뜨리면 파드 안의 psql 이 빈 입력을 받고 정상 종료하고, 셸은 오류를 내지 않고, 종료 코드도 0 이며, date 두 줄은 「1초 만에 끝났다」로 찍힌다. 복구된 것과 구별되지 않는다.
kubectl -n keycloak-lab exec deploy/postgres -- psql ... < dump.sql # ✘
kubectl -n keycloak-lab exec -i deploy/postgres -- psql ... < dump.sql # ✔
위 두 줄은 치는 명령이 아니다. ... 와 dump.sql 은 두 형태를 나란히 놓으려고 줄여 쓴 것이고, 실제로 치는 복구 명령은 전제 절과 아래 복구 §1 에 온전한 형태로 있다. -i 는 표준입력을 파드 안으로 연결하라는 뜻이다. 구별하는 유일한 방법이 복구 뒤의 데이터 대조이고, 그래서 대조는 선택이 아니다.
관찰
전부 깨지지는 않는다. 세 경로를 나눠서 친다.
curl -s -o /dev/null -w 'certs %{http_code}\n' \
https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs
curl -s -o /dev/null -w 'well-known %{http_code}\n' \
https://auth.hyeonworks.com/realms/keycloak-patterns/.well-known/openid-configuration
실측은 이렇다(observed, 03-restore.txt).
/.well-known/openid-configuration HTTP 500
/protocol/openid-connect/certs HTTP 200
토큰 발급 (DB 쓰기 필요) HTTP 400
토큰 발급은 값이 필요하므로 따로 친다. 이 실험대는 스크립트로 돌렸고(observed), 아래는 가이드가 미검증으로 표시한 형태다(unknown).
curl -s -o /dev/null -w '토큰 %{http_code}\n' -X POST \
https://auth.hyeonworks.com/realms/master/protocol/openid-connect/token \
-d grant_type=password -d client_id=admin-cli -d username=admin \
-d "password=$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)"
비밀번호를 화면에 찍지 않는다. 명령 치환으로 넘기므로 값은 터미널에도 셸 히스토리에도 남지 않는다. 길이만 확인하려면 한 줄을 더 친다.
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
세 값이 다 다르다.
| 경로 | 코드 | 왜 |
|---|---|---|
certs (JWKS) |
200 | realm 키가 캐시에 있다. 데이터베이스를 안 본다 |
.well-known |
500 | 이 응답을 만들려면 데이터베이스를 본다 |
| 토큰 발급 | 400 | 세션을 써야 한다 |
부분적으로만 깨진다. 헬스체크는 통과하고, 일부 엔드포인트는 정상이며, 로그인만 안 된다. 운영에서 이 모양이 고약한 까닭은 「사이트가 떴는가」를 재는 감시가 전부 초록인데 사용자만 못 들어오기 때문이다. 이 사고의 감시 항목은 /realms/master 가 아니라 토큰 발급이어야 한다.
로그가 이유를 말한다.
kubectl -n keycloak-lab logs keycloak-0 --tail=50
실측은 이렇다(observed, 02-destruction.txt).
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)
WARN 이지 ERROR 가 아니다. 내용은 클러스터 멤버를 못 가져온다는 것이고, JGROUPS_PING 테이블도 같이 지워졌기 때문이다(A-1 에서 그 테이블을 봤다). 디스커버리가 깨졌는데도 로그 레벨이 WARN 이라 대시보드의 에러 카운터에 안 잡힐 수 있다. 정문의 200, 부분 정상, 여기의 WARN — 세 관측이 전부 「괜찮다」 쪽으로 기운다.
「데이터베이스가 살아 있다」와 「데이터가 있다」는 다르고, 그 차이가 이 실험의 모양을 만든다.
A-2 DB 프로세스 정지 → 커넥션 실패 → readiness DOWN → 파드가 Service 에서 빠진다
D-1 스키마만 삭제 → 커넥션 정상 → readiness UP → ★ 파드가 그대로 트래픽을 받는다
헬스체크는 커넥션만 본다. 그래서 빈 데이터베이스를 통과시킨다. Keycloak 의 버그가 아니다 — 「데이터베이스에 붙을 수 있는가」는 프로브가 답할 수 있는 물음이고 「데이터가 온전한가」는 프로브가 답할 수 없는 물음이다. 뒤엣것을 재려면 업무 트랜잭션 하나를 실제로 돌리는 감시가 따로 있어야 한다.
| 재는 것 | 이 사고에서 |
|---|---|
파드 Ready |
초록 |
정문 200 |
초록 |
JWKS 200 |
초록 |
| 토큰 발급 | 400 ← 유일하게 정직한 지표 |
복구와 원상복구 확인표
1. 덤프를 되돌린다
목적 — 파괴 전의 데이터로 되돌리고, 질문 ②의 답을 만든다.
① 시각을 남기고 복구한다. -i 가 있는지 치기 전에 눈으로 확인한다.
date '+%H:%M:%S 복구 시작'
kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak \
< /tmp/keycloak-backup.sql > /tmp/restore.log 2>&1
date '+%H:%M:%S 복구 완료'
예상 결과 — 실측은 이렇다(observed, 03-restore.txt).
시작: 15:00:12
완료: 15:00:13
오류 줄: 0
② 로그의 오류를 센다.
grep -ci '^ERROR' /tmp/restore.log
tail -5 /tmp/restore.log
예상 결과 — 0 이어야 한다. 0 이 아니면 어떤 줄이 실패했는지 본다. --clean --if-exists 로 뜬 덤프를 빈 데이터베이스에 넣으면 오류가 0 인 것이 정상이다.
왜 필요한가 — 시각 두 줄과 오류 0건은 -i 를 빠뜨렸을 때도 똑같이 나온다. 그래서 이 둘로는 복구를 판정하지 않는다.
2026-09-17 에 이 절차를 처음부터 끝까지 쳤다(observed).
덤프 101 테이블 · 976873 bytes · 8569 줄
파괴 15:31:02 DROP SCHEMA · CREATE SCHEMA → 남은 테이블 0 · select count(*) from realm 이 relation does not exist
그동안 밖에서는 200 ← 스키마가 통째로 없는데 정문은 멀쩡했다
복구 15:31:02 → 15:31:08 (6초) · 복구 로그 ERROR 0
복구 뒤 101 테이블 · 세션 6건 — 파괴 전과 같다
크기와 줄 수는 실험대마다 다르다. 위 실측의 394945 bytes · 6956 줄 은 그 실험대의 값이고, BFF 쪽 테이블이 있는 이 실험대에서는 976873 bytes · 8569 줄 이었다. 같아야 하는 것은 테이블 수 101 과 복구 전후의 행 수다.
스키마가 없는 동안에도 정문이 200 인 것이 이 편의 핵심이다. Keycloak 은 이미 읽어 둔 것으로 답하므로, 밖에서 보는 코드만으로는 데이터베이스가 통째로 비었다는 것을 알 수 없다.
문제가 생기면 — 1초 만에 끝났는데 다음 단계의 대조가 어긋나면 -i 를 의심한다.
2. 진짜 판정 — 주입 전과 문자 단위로 견준다
목적 — 복구가 완전 일치인지 가른다.
① 주입 전에 친 것과 똑같은 명령을 친다.
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')"
예상 결과 — 실측은 이렇다(observed, 03-restore.txt). 원래 실행은 스크립트였으므로 이름표가 붙은 두 줄이고 값이 다섯이다. 위 명령을 손으로 치면 이름표 없이 2|15|2|3 한 줄만 나온다. 주입 전 §2 에서 복사해 둔 줄과 그 한 줄을 견준다.
복구 후: realms|clients|users|sessions|authclients = 2|15|2|3|1
백업 시: realms|clients|users|sessions|authclients = 2|15|2|3|1
왜 필요한가 — 두 줄이 문자 단위로 같은가를 본다. 완전 일치이고, 질문 ②의 답이 「돈다」인 근거가 이 두 줄이다. 여기가 다르면 그 앞의 모든 성공 표시는 무의미하다.
문제가 생기면 — -i 를 빠뜨렸는지 먼저 의심하고, 붙여서 다시 친다.
3. 손대지 않고 기다린다
목적 — 스스로 회복하는지 본다. 여기서 파드를 재시작하면 그 물음 자체가 사라진다.
① 15초쯤 뒤에 본다.
curl -s -o /dev/null -w 'well-known %{http_code}\n' \
https://auth.hyeonworks.com/realms/keycloak-patterns/.well-known/openid-configuration
kubectl -n keycloak-lab get pods -o wide | grep keycloak
예상 결과 — 실측은 이렇다(observed, 03-restore.txt).
+15초 well-known=200 토큰발급=200
→ 재시작 없이 회복
keycloak-0 restarts=0
keycloak-1 restarts=0
왜 필요한가 — 500 이던 well-known 이 200 이 된 것과 RESTARTS 가 여전히 0 인 것을 같이 본다. 200 을 본 순간의 시각을 손으로 적어 둔다 — 아래 §5 의 복구 시간 목표가 끝나는 지점이 그 시각인데, 이 단계에는 그것을 남기는 date 줄이 가이드에 없다. 커넥션 풀이 이미 붙어 있었으므로 테이블이 돌아오자마자 동작했다. 파드를 만졌다면 「복구 절차에 파드 재시작이 필요하다」는 잘못된 절차가 문서에 남았을 것이다.
4. 세션이 살아났는지 본다
목적 — 덤프의 COPY 블록에서 본 id 가 테이블로 넘어왔는지 눈으로 잇는다.
① 세션 행을 다시 나열한다. 주입 전 §3 에서 친 것과 열이 하나 다르다 — 거기는 realm_id 까지 셋을 뽑고 여기는 user_session_id 와 offline_flag 둘만 뽑는다. 가이드 원문이 그렇게 갈려 있어 그대로 싣는다. 열이 다르므로 행 수와 user_session_id 값으로 견준다.
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select user_session_id, offline_flag from offline_user_session"
예상 결과 — 실측은 이렇다(observed). 원래 실행은 realm 이름을 함께 뽑았으므로 아래 둘째 열이 realm 이다. 위 명령을 그대로 치면 둘째 열에 offline_flag 가 오고 값은 0 으로 찍힌다. 열 이름이 다른 것을 복구가 덜 됐다는 신호로 읽지 않는다 — 견줄 것은 user_session_id 네 값과 (4 rows) 다.
user_session_id | realm
--------------------------+-------------------
E1q5xI7tt4U_WhZpW7rEPIF2 | master
2ap3DyRiBF8OdMiqCodsJ0mp | master
Zsk4QcgXf_qgyMKzde5AG-Fz | master
vsDgCVo12-qX0CC63ZmYzbYF | keycloak-patterns
(4 rows)
왜 필요한가 — E1q5xI7tt4U_WhZpW7rEPIF2 가 덤프의 COPY 블록에도 복구된 테이블에도 있다. 파일에서 데이터베이스로 실제로 넘어온 것을 눈으로 잇는다. 세션이 백업에서 복원되고 로그인 상태가 유지된다.
5. 적어 둔 시각 셋을 나란히 놓는다
14:59:47 파괴
15:00:12 복구 시작
15:00:13 복구 완료
~15:00:28 서비스 정상 확인
RTO = 41초
값은 넷인데 date 가 남기는 것은 셋이다. 파괴·복구 시작·복구 완료 세 줄만 명령이 찍고, 넷째 ~15:00:28 서비스 정상 확인 은 §3 에서 사람이 읽어 적은 시각이다. RTO = 41초 는 첫째와 넷째의 차이므로, 그 시각을 안 적어 뒀으면 여기서 복구 시간 목표를 못 만든다.
41초 중 복구 명령 자체는 1초다. 나머지는 파괴를 알아채고 무엇을 할지 정하는 시간이며, 이 실험에서는 이미 알고 있었으므로 25초였다. 실제 사고에서는 이 부분이 대부분을 차지한다.
복구 지점 목표는 두 겹이다.
① 마지막 덤프 이후의 모든 변경 ← 백업 주기가 정한다
② A-3 에서 측정한 synchronous_commit 손실 ← 수백 ms
실제 RPO = ① + ②
A-3 은 클라이언트가 200 을 받은 로그인 153건 중 4건이 데이터베이스에 없었다는 것을 측정했다. 백업 주기만 보고 복구 지점 목표를 말하면 ②를 빠뜨린다.
그리고 이 실험대의 규모는 현실적이지 않다.
| 이 실험대 | 운영 | |
|---|---|---|
| 덤프 크기 | 395KB | GB~TB |
| 복구 시간 | 1초 | 분~시간 |
| 세션 수 | 3~4 | 수만 |
복구가 1초인 것은 데이터가 작기 때문이고, 이 실험이 확인한 것은 절차가 맞다는 것까지다. 시간은 규모에 따라 완전히 달라진다.
6. 덤프를 다른 기계로 옮긴다 — 이 실험이 「못 했다」로 남긴 단계
목적 — 덤프를 데이터베이스와 다른 장애 도메인에 둔다.
사람이 쳐야 하는 부분이 여기서 갈린다.
| 하는 일 | 어디서 | sudo |
|---|---|---|
| 덤프 뜨기 · 복구 | kc-lab-1 |
게스트는 무암호 — 스크립트로도 된다 |
| 덤프를 호스트의 사용자 홈에 두기 | test-server |
필요 없다 |
덤프를 root 소유 경로(/var/backups 등)에 두기 |
test-server |
비밀번호를 묻는다 — 사람이 친다 |
호스트에서 비대화 sudo 는 반드시 실패한다. 그 벽에 부딪힌 기록이 D-4 의 증거에 남아 있다(observed, d4-certificate-renewal/01-certificate-state.txt).
$ sudo -n -l
sudo: a password is required
-n 은 비밀번호를 물어보지 말라는 뜻이고 호스트에서는 그게 곧 실패다. 그러므로 백업을 호스트의 보호된 경로에 두는 단계는 자동화할 수 없다. ssh -t 로 붙어 사람이 비밀번호를 쳐야 하고, -t 가 없으면 sudo 가 비밀번호를 읽을 tty 가 없다.
① 두 줄을 차례로 친다. 이 실험대는 여기까지 하지 않았다(unknown). 가이드가 미검증으로 표시한 줄이고, 호스트 이름과 경로는 따라 하는 사람의 배치에 맞춘다.
# ① kc-lab-1 에서 호스트로 — sudo 없이 사용자 홈에
scp /tmp/keycloak-backup.sql test-server:~/keycloak-backup-2026-09-04.sql
# ② 보호된 경로로 옮기는 것은 호스트에서 사람이 친다 (비밀번호 프롬프트)
ssh -t test-server 'sudo install -m600 -o root -g root \
~/keycloak-backup-2026-09-04.sql /var/backups/keycloak-backup-2026-09-04.sql'
② 옮긴 파일이 온전한지는 크기를 양쪽에서 세서 비교한다.
wc -c /tmp/keycloak-backup.sql
ssh test-server 'wc -c ~/keycloak-backup-2026-09-04.sql'
예상 결과 — 두 숫자가 같다. 다르면 전송이 잘린 것이다.
왜 필요한가 — 이것으로도 부족하다. 호스트는 VM 두 대를 품고 있는 기계이므로 호스트가 죽으면 게스트도 덤프도 같이 간다. 진짜 요건은 「다른 기계」가 아니라 「다른 장애 도메인」이다.
이 두 줄은 호스트에 평문 덤프를 두 벌 남긴다. install 은 옮기기가 아니라 복사라 사용자 홈의 keycloak-backup-2026-09-04.sql 이 /var/backups 의 사본과 함께 그대로 있다. 두 파일 다 realm·client·user·세션을 통째로 담고 있는데, 지우는 절차는 가이드에 없고(unknown) 아래 여덟 항목에도 없다. 실험대 밖에서 이 단계를 밟았다면 두 파일을 어떻게 할지는 치는 사람이 정한다.
7. 여덟 항목을 대조한다
| 항목 | 명령 | 돌아왔을 때 |
|---|---|---|
| 테이블 | psql -c "select count(*) from pg_tables where schemaname='public'" |
101 |
| 데이터 | 복구 대조의 -tAc 한 줄 |
백업 시점과 문자 단위로 동일 |
| 세션 | select count(*) from offline_user_session |
파괴 전과 같은 수 |
| 파드 | kubectl -n keycloak-lab get pods -o wide |
1/1 Running, RESTARTS 0 |
| 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 |
| 로그인 | 관찰 절의 토큰 발급 | 200 ← 이것이 진짜 판정 |
| 덤프 | ls -l /tmp/keycloak-backup.sql |
남겨 둔다. D-2 의 전제다 |
덤프를 지우지 않는다. D-2 가 이 파일을 전제로 한다.
막히면
가이드는 이 표를 두고 전부 이 실험대가 실제로 겪은 증상이거나 이 절차에서 실제로 갈리는 곳이라고 적는다.
| 증상 | 원인 | 확인 |
|---|---|---|
| 복구가 1초 만에 끝났는데 데이터가 없다 | exec 에 -i 가 없다. 오류도 안 난다 |
데이터 대조. -i 를 붙여 다시 |
복구에서 already exists 가 쏟아진다 |
덤프를 --clean --if-exists 없이 떴다 |
grep -c '^DROP TABLE' /tmp/keycloak-backup.sql — 0 이면 그것 때문이다 |
| 덤프 파일은 있는데 복구가 중간에 멈춘다 | 덤프가 잘렸다 | tail -3 에 dump complete 가 있는가 |
파괴했는데 정문이 계속 200 |
정상이다. realm 캐시가 서빙한다 | 토큰 발급으로 판정 |
psql: relation "realm" does not exist |
파괴가 걸린 것이다 | 그게 주입 검증의 기대 출력이다 |
kubectl get endpoints 가 경고를 찍는다 |
v1.33+ 에서 deprecated | get endpointslice -l kubernetes.io/service-name=... |
kubectl exec keycloak-0 -- curl 이 exit 127 |
Keycloak 이미지에 curl 도 wget 도 없다 | 밖에서 curl 로 친다 |
| 세션 개수가 나열한 행 수와 다르다 | 개수 쿼리에 offline_flag='0' 필터가 있다 |
같은 쿼리끼리 비교 |
| 백업이 0바이트다 | pg_dump 가 인증에서 막혔다 |
-U keycloak -d keycloak 를 확인. 파일을 지우고 다시 뜬다 |
호스트에서 sudo 가 안 먹는다 |
호스트 sudo 는 비밀번호를 요구한다 | ssh -t 로 붙어 사람이 친다 |
무엇이 관측이고 무엇이 아닌가
이 절차의 숫자는 2026-09-04 14:57–15:00 KST 에 돈 한 번의 실행에서 나왔다(observed).
- (observed) 파괴 직후 파드 네 줄과
RESTARTS 0, 백업의시작: 14:59:30·완료: 14:59:30·크기: 394945 bytes (6956 줄), 테이블 수101,offline_user_session 언급: 13과COPY블록에 붙은 세션 행, 파괴 시각14:59:47과DROP SCHEMA·CREATE SCHEMA, 남은 테이블0, 파괴 뒤에도 정문과 app1 이 전부HTTP 200인 것,certs200 ·.well-known500 · 토큰 발급 400,KEYCLOAK_JDBC_PING2의WARN두 줄, 복구의시작: 15:00:12·완료: 15:00:13·오류 줄: 0, 복구 전후 대조 두 줄이 같은 것,+15초 well-known=200 토큰발급=200과restarts=0, 복구된 세션 네 행,RTO = 41초. - (observed) A-3 이 잰 로그인 153건 중 4건 소실은 그 실험의 값이고, 여기서는 실제 복구 지점 목표의 두 번째 겹으로 인용만 한다.
- (unknown) 남은 테이블을 세는
pg_tables쿼리와 토큰 발급curl한 줄. 가이드가 미검증으로 표시했고 원래 실행은 스크립트로 돌렸다. 덤프를 호스트로 옮기는 두 줄도 미검증이고, 이 실험대는 그 단계를 하지 않았다 — 덤프는 데이터베이스와 같은 기계에 놓인 채 실험이 끝났다. - 다섯째 컬럼은 지어내지 않았다. 증거 줄에는
authclients까지 다섯 값이 있는데 해설 문서의 재현 절차에 그 쿼리가 없어서, 가이드도 이 절차도 넷만 센다. - 비밀은 옮기지 않았다 — 관리자 비밀번호는 명령 치환으로만 넘어가고, 길이를 재는 줄만 따로 있다. 세션 id 와 realm UUID 는 식별자라 그대로 적었다. 덤프 파일 자체가 realm·client·user·세션을 통째로 담고 있고, 그 파일을 어디에 두는가가 이 실험의 마지막 물음이다.
- (unknown) 검증 넷을 통과한 덤프로 복구했는데 그 복구가 실패했을 때 갈 길은 가이드에 없다. 「막히면」 표는 원인을 가리키는 데까지만 적고, 지우고 다시 시작하는 절차를 주지 않는다. 만들어 넣지 않았다.
- 이 실험이 확인하지 않은 것 — 백업 자동화, 보존 주기, 복구 리허설의 정기 실행. 이번엔 손으로 한 번 떴고 한 번 되돌렸다. 그것만 참이다.