저장소만 있으면 블로그가 다 살아날 줄 알았는데, 레포 밖에 설정이 여섯 가지 있었습니다
제 블로그는 Hugo(마크다운을 HTML로 바꿔 주는 정적 사이트 생성기)로 만들고 Cloudflare Pages에 올립니다. 글도 템플릿도 설정도 전부 Git 저장소에 있으니 백업은 이미 끝난 셈이라고 생각해 왔습니다. 노트북이 망가져도 clone 한 번이면 된다고 봤습니다.

그런데 실제로 해 본 적은 없었습니다.
그래서 복구 연습을 한 번 했습니다. 운영 중인 사이트는 읽기만 하고 저장소를 새로 받아 빌드하고
저장소 바깥에 있는 설정을 하나씩 꺼내 보고 미리보기 배포로 첫 응답까지 시간을 쟀습니다.
2026-10-07 0시대(한국 시각)에 Hugo v0.165.0 extended로 한 작업입니다.
새로 clone한 빌드는 라이브와 바이트까지 같았습니다

먼저 저장소만으로 사이트가 그대로 나오는지 봤습니다. 운영 체크아웃에서 Git이 모르는 파일부터 확인했습니다.
$ git status --ignored --short
!! .config/
!! .hugo_build.lock
.config/에는 배포용 Cloudflare API 토큰이 들어 있습니다. 나머지는 빌드할 때 생기는 잠금 파일입니다.
빌드 결과물 폴더 public/이나 이미지 캐시 resources/도 로컬에 남아 있지 않았습니다.
로컬 디스크에만 있는 글이나 이미지는 없다는 뜻입니다.
그다음 저장소를 새로 clone해서 빌드하고 결과물의 파일을 하나씩 라이브 같은 주소에서 받아 비교했습니다.
$ git clone -q file:///…/birchholt-site full
$ cd full && ./build.sh # hugo --gc --minify
Pages │ 24
Total in 79 ms
$ find public -type f | wc -l
172
라이브와 바이트 비교(cmp) 같음 171 · 다름 0 (_headers 1개는 응답으로 안 나가서 제외)
171개 전부 같았습니다. sitemap.xml도 같았습니다. Hugo 공식 포럼에도 “Git 저장소에 있는 것만으로 새 버전을 배포하기에 충분하다"는 답이 달려 있는데 (Hugo Discourse), 사이트 파일만 보면 그 말이 맞았습니다.
단, 얕게 받으면 달라집니다. git clone --depth 1(최근 커밋 하나만 받는 clone)로 같은 빌드를 하자
글 14편의 HTML과 sitemap.xml, 모두 15개 파일이 달라졌습니다.
제 설정은 Git 커밋 시각으로 글의 수정일을 정하는데 이력이 없으니 다른 값이 들어간 것입니다.
원인과 처리는 sitemap lastmod 글에 적어 뒀습니다.
복구할 때도 전체 이력으로 받아야 라이브와 같은 결과가 나옵니다.
저장소 밖에 있던 것을 세어 보니 여섯 가지였습니다

사이트 파일은 저장소가 책임집니다. 하지만 주소창에 birchholt.com을 쳐서 그 파일이 나오기까지는
저장소에 없는 설정이 여럿 끼어 있습니다. 제 경우를 세어 보니 이랬습니다.
| 레포 밖 설정 | 어디에 있나 | 이번에 꺼낼 수 있었나 |
|---|---|---|
| DNS 레코드 4개(CNAME 3, TXT 1) | Cloudflare 존 | 예 — 파일로 내보냄 |
| Pages 프로젝트(이름·운영 브랜치·커스텀 도메인 2개) | Cloudflare 계정 | 예 — JSON |
| www → 루트 리다이렉트 규칙 | Cloudflare 존 규칙 | 아니요 — 403 |
| GitHub 시크릿 2개 | 저장소 설정 | 이름만(README에 적어 둠), 값은 못 꺼냄 |
| production 배포 이력 173건 | Pages 프로젝트 안 | 목록만 |
R2 버킷(media. 레코드가 가리키는 저장소) | Cloudflare 계정 | 아니요 — 403 |
TXT 레코드 하나는 Search Console 소유확인 값입니다.
네이버 서치어드바이저 쪽 소유확인 값은 반대로 hugo.toml에 들어 있어서 저장소가 챙깁니다.
같은 성격의 값인데 하나는 저장소 안, 하나는 저장소 밖에 있었습니다. 표를 만들기 전에는 몰랐습니다.
R2 버킷(Cloudflare의 파일 저장소)은 DNS에 media.birchholt.com 레코드로만 이어져 있습니다.
이 사이트의 빌드 결과물은 그 주소를 한 번도 참조하지 않았고(grep 결과 0건) 지금 그 주소는 404를 돌려줍니다.
사이트 복구와는 무관하지만 DNS를 내보내면 이 레코드도 따라오니 표에는 넣어 뒀습니다.
버킷 목록은 403이라 몇 개인지도 보지 못했습니다.
꺼낼 수 있는 것은 GET 요청 몇 번이었습니다

DNS는 Cloudflare가 내보내기 기능을 줍니다. 문서에 “API로 내보내려면 GET 요청을 보내라"고 되어 있고 형식은 BIND 존 파일(DNS 서버들이 쓰는 표준 텍스트 형식)입니다 (Cloudflare DNS · Import and export records).
curl -s -H "Authorization: Bearer $CF_TOKEN" \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/export" > dns.zone
birchholt.com. 1 IN CNAME birchholt.pages.dev. ; cf_tags=cf-proxied:true
media.birchholt.com. 1 IN CNAME public.r2.dev. ; cf_tags=cf-proxied:true
www.birchholt.com. 1 IN CNAME birchholt.pages.dev. ; www experiment 2026-10-04 cf_tags=cf-proxied:true
birchholt.com. 3600 IN TXT "google-site-verification=…"
받아 보니 생각보다 많이 담겨 있었습니다.
레코드마다 Cloudflare 프록시를 켰는지(cf-proxied), 그리고 제가 대시보드에서 달아 둔 메모(www experiment 2026-10-04)까지 실려 있습니다.
문서에도 내보낼 때 이 태그가 현재 설정을 반영해 붙는다고 적혀 있습니다.
파일 맨 위에는 “보관용이며 실제 DNS 서버에 쓰기 전에 고쳐야 한다"는 머리말이 붙어 나옵니다.
되살리는 쪽은 실제로 해 보지 않았습니다. 운영 중인 존을 건드리면 안 되기 때문입니다.
Pages 프로젝트 설정도 같은 토큰으로 JSON 한 장에 나왔습니다.
production_branch main
domains birchholt.pages.dev, birchholt.com, www.birchholt.com
source null ← Git 연결 없이 파일을 직접 올리는 프로젝트
env_vars null ← preview·production 모두 환경 변수 없음
source가 null이라는 게 중요했습니다.
Pages가 저장소를 직접 보지 않고, GitHub Actions가 빌드한 파일을 받기만 합니다.
그래서 빌드 설정(명령, Hugo 버전)은 Pages가 아니라 저장소의 워크플로 파일과 build.sh에 있고 저장소가 챙깁니다.
환경 변수도 없으니 Pages 쪽에서 잃을 설정이 적습니다.
리다이렉트 규칙은 배포 토큰으로 읽을 수조차 없었습니다

걸린 건 여기였습니다. 같은 토큰으로 나머지를 요청했더니 이렇게 나왔습니다.
GET zones/<ZONE>/rulesets 403 Authentication error
GET zones/<ZONE>/settings/always_use_https 403
GET zones/<ZONE>/settings/ssl 403
GET accounts/<ACCOUNT>/r2/buckets 403
www.birchholt.com을 루트로 보내는 규칙은 www 리다이렉트 글에서
대시보드의 템플릿으로 만든 것입니다. 그 글에는 어떤 템플릿을 골랐는지만 적었고 규칙의 조건식과 대상 주소 자체는 저장소에도 메모에도 남기지 않았습니다.
그리고 지금 손에 있는 토큰은 배포에 필요한 권한만 갖고 있어서 규칙을 읽는 것조차 안 됩니다.
권한을 최소로 준 건 잘한 일이지만 그 결과 이 규칙은 정의를 꺼내 둔 사본이 없는 설정이었습니다.
정의를 못 꺼내니 대신 동작을 적어 두기로 했습니다. 규칙이 사라져도 무엇을 되살려야 하는지는 알 수 있게요.
http://birchholt.com/ 301 → https://birchholt.com/
https://www.birchholt.com/posts/…/?utm=x 301 → https://birchholt.com/posts/…/?utm=x
https://birchholt.com/__nope__/ 404
https://birchholt.pages.dev/ 200 x-robots-tag: noindex
두 번째 줄에서 경로와 쿼리 문자열이 그대로 넘어간다는 것까지 남습니다. 되살린 규칙이 이걸 빠뜨리면 같은 301이어도 결과가 다릅니다.
GitHub 시크릿도 비슷합니다. GitHub API 문서는 저장소 시크릿을 조회할 때 “암호화된 값을 드러내지 않고” 돌려준다고 적고 있습니다 (GitHub REST · Get a repository secret). 이름은 README에 표로 적어 뒀지만 값은 GitHub에서도 다시 볼 수 없습니다. 토큰을 잃으면 복구가 아니라 재발급이 됩니다.
미리보기 배포로 재 보니 첫 200까지 7초였습니다

마지막으로 “새로 받아서 다시 올리면 몇 분 걸리나"를 쟀습니다.
운영 주소는 건드리지 않고 같은 Pages 프로젝트에 restore-drill이라는 이름의 미리보기(운영과 분리된 임시 주소) 배포로 올렸습니다.
00:03:07 시작
git clone 0초
./build.sh 1초
wrangler pages deploy --branch restore-drill 5초
Uploaded 0 files (171 already uploaded)
첫 200 (restore-drill.birchholt.pages.dev) 1초
합계 7초
받은 페이지를 로컬 결과물과 비교하니 홈, sitemap, 404 페이지, 글 하나가 모두 같았습니다.
미리보기 주소에도 noindex 헤더가 붙어 있어 검색에 섞일 걱정은 없었습니다.
다만 이 7초를 그대로 믿으면 안 됩니다. 로그의 0 files가 그 이유입니다.
같은 프로젝트에 이미 올라간 파일과 내용이 같으니 하나도 다시 올리지 않았습니다.
프로젝트가 살아 있을 때의 시간입니다. 프로젝트까지 잃은 경우는 새로 만들 수 없어서 재지 못했습니다.
그 경우에는 커스텀 도메인 연결을 다시 해야 하는데, 제 기록으로는 www 도메인 하나를 붙이는 데 약 1분 41초가 걸렸습니다(www 리다이렉트 글). 같은 존에 DNS 레코드가 이미 있던 상태의 시간입니다.
배포 이력도 생각해야 합니다. Pages의 롤백은 “이전 production 배포로 즉시 되돌리는” 기능이고 성공한 production 배포만 대상이 됩니다 (Cloudflare Pages · Rollbacks). 제 프로젝트에는 production 배포가 173건 쌓여 있습니다. 이 목록은 프로젝트 안에만 있으니, 프로젝트를 지우면 같이 사라진다고 봐야 합니다(추론이고, 지워 보지는 않았습니다). 그래도 사이트 자체는 커밋에서 바이트 단위로 다시 만들 수 있으니, 제게는 롤백 이력이 없어도 되는 쪽이었습니다.
시험이 끝나고 이 미리보기 배포는 API로 지웠습니다.
API로 다시 조회하면 바로 404가 나왔지만 restore-drill.birchholt.pages.dev 주소는 제가 확인을 마칠 때까지 약 9분 동안 계속 200을 돌려줬습니다.
원인은 확인하지 못했습니다. 지운 즉시 주소까지 닫힌다고 기대하지는 않는 게 좋겠습니다.
석 달에 한 번 이 스크립트를 돌리기로 했습니다

이번에 한 일을 GET 요청만 하는 스크립트 하나로 묶었습니다. 실행에 3.5초 걸렸습니다.
#!/usr/bin/env bash
# 레포 밖 설정 스냅샷 — GET 만 한다. 결과 폴더는 커밋하지 않는다(계정 ID가 들어 있다).
set -euo pipefail
: "${CF_TOKEN:?}" "${ZONE_ID:?}" "${ACCOUNT_ID:?}"
OUT="snapshot-$(date +%F)"; mkdir -p "$OUT"
API=https://api.cloudflare.com/client/v4
get(){ curl -sf -H "Authorization: Bearer $CF_TOKEN" "$API/$1" -o "$OUT/$2" && echo "ok $2" || echo "FAIL $2"; }
get "zones/$ZONE_ID/dns_records/export" dns.zone
get "accounts/$ACCOUNT_ID/pages/projects/birchholt" pages-project.json
get "accounts/$ACCOUNT_ID/pages/projects/birchholt/domains" pages-domains.json
get "zones/$ZONE_ID/rulesets" rulesets.json
# 읽을 수 없는 설정은 '동작'으로 남긴다
for u in http://birchholt.com/ https://www.birchholt.com/a/?q=1 https://birchholt.com/__nope__/ https://birchholt.pages.dev/; do
curl -s -o /dev/null -w "%{http_code} %{redirect_url} ← $u\n" "$u"
done > "$OUT/behavior.txt"
git -C "${REPO:-.}" bundle create "$PWD/$OUT/repo.bundle" --all 2>/dev/null && echo "ok repo.bundle"
ok dns.zone
ok pages-project.json
ok pages-domains.json
FAIL rulesets.json
ok repo.bundle
git bundle(저장소 전체 이력을 파일 하나로 묶는 명령)로 만든 7.5MB 파일에서 다시 clone해 빌드해 봤는데 결과물이 처음 빌드와 똑같았습니다.
GitHub에 접근할 수 없을 때를 대비한 사본입니다.
FAIL은 일부러 남겨 뒀습니다. 리다이렉트 규칙이 아직 사본 없는 설정이라는 표시입니다.
읽기 권한이 있는 토큰을 따로 만들거나 규칙 내용을 직접 적어 두면 이 줄이 ok로 바뀔 겁니다.
석 달마다 이렇게 봅니다.
- 전체 이력으로 clone해 빌드하고 라이브와 비교합니다.
--depth 1로 받으면 결과가 달라집니다 - 스크립트를 돌려
FAIL줄을 봅니다. 그 줄이 사본 없는 설정 목록입니다 behavior.txt를 지난번 파일과 비교합니다. 리다이렉트 동작이 바뀌었으면 여기서 보입니다- 결과 폴더는 커밋하지 않습니다. 계정 ID와 소유확인 값이 들어 있습니다
- 시크릿은 이름과 발급처, 만료일만 적어 둡니다. 값은 못 꺼내니 다시 만드는 방법을 적어 두는 게 맞습니다
이번 연습에서 사이트 파일 복구는 걱정할 필요가 없다는 걸 확인했습니다. 신경 써야 하는 건 저장소 바깥에 있는 설정이었고 그중 하나는 사본이 아예 없었습니다.