Watch at Ease세상을 읽고, 일상의 답을 찾다.

기술 · 실용 가이드

Restic에서 pack 누락이 나오면: 저장소 변경 전 복구 원장을 만드세요

증거를 보존하고 restic이 기록한 검사 결과를 분류한 뒤, index나 snapshot을 고치기 전에 가장 덜 파괴적인 다음 단계를 정합니다.

보호된 백업 저장소가 읽기 전용 복구 원장으로 이어지고 변경 게이트는 닫혀 있는 원본 개념 일러스트.
원본 개념 일러스트: 어떤 복구 변경을 승인하기 전에 저장소 증거를 보존하고 check 결과를 분류합니다. Watch at Ease

restic pack 누락이나 손상은 단순 정리 경고가 아닙니다. 완전한 check가 반대를 입증하기 전까지 restore가 실패할 수 있고 새 snapshot도 안전하게 복원된다고 볼 수 없습니다. 예약 backup, forget, prune을 멈추고 원본 출력을 보존하며, 복구 전에 저장소 전체 또는 최소한 index와 snapshots를 복사합니다.

이 가이드는 공식 `restic check --json` summary, 따로 저장한 stderr, snapshot repair dry-run을 읽기 전용 복구 원장으로 묶습니다. 포함된 parser는 저장소를 열거나 restic을 실행하거나 자료를 삭제하거나 변경을 승인하지 않습니다.

원장은 broken pack, index 복구 제안, 해결되지 않은 기타 오류, 선택적 정리, clean summary를 구분합니다. 그래도 사용 중인 restic 버전이 실제 출력한 지시를 따르고 backend 또는 hardware 원인을 조사하며 대표 restore로 복구 가능성을 입증해야 합니다.

1. 변경을 멈추고 최초 증거를 보존합니다

이 저장소의 예약 backup, forget, prune을 끕니다. 분류 전에는 실패한 prune을 계속하거나 pack을 수동 삭제하거나 index를 재구축하거나 사용 중인 lock을 해제하거나 snapshot repair를 시작하지 않습니다. lock error와 확인된 stale lock은 다른 문제입니다.

정확한 restic version, command, exit status, stdout, stderr를 저장합니다. 최초 오류에는 후속 command에서 사라지는 pack ID나 backend error가 있을 수 있습니다. 공개 전 repository URL, username, path를 가리고 password, token, cloud credential은 절대 포함하지 않습니다.

가능하면 저장소 전체를 복사합니다. 공식 최소 fallback은 index와 snapshots folder 복사입니다. 두 번째 저장소에 새 backup을 시작하면 운영 압박은 줄지만 손상 저장소가 고쳐졌다는 증거는 아닙니다.

2. stderr를 합치지 말고 machine summary를 수집합니다

기존 보안 방식으로 repository와 password를 설정한 뒤 비용과 시간에 맞는 check를 선택합니다. default check는 구조를 검증하고 `--read-data`는 모든 pack을 읽어 시간과 bandwidth가 많이 들 수 있습니다. subset check는 유지 관리에 유용해도 사고 종료를 위한 전체 coverage를 대신하지 못합니다.

공식 JSON interface는 final summary를 stdout, error object를 stderr에 기록하므로 따로 보존합니다. 아래는 간단한 POSIX shell capture이며 exit code 줄은 check 직후에 와야 합니다.

```sh restic version > restic-version.txt restic check --json > check-summary.json 2> check-errors.jsonl printf '%s\n' "$?" > check-exit.txt ```

`check-summary.json`에 완전한 summary object 하나가 있는지 확인합니다. summary가 충분해 보여도 `check-errors.jsonl`을 남깁니다. free-form message는 바뀔 수 있으므로 자동화는 documented field로 분기하고 사람을 위해 원본 error evidence를 보존합니다.

3. 읽기 전용 의사결정 원장을 만듭니다

아래 전체 코드를 `restic_check_ledger.py`로 저장하세요. 저장한 요약만 읽으며 최종 경로 요소가 심볼릭 링크이거나 일반 파일이 아니면 거부합니다. 최대1 MiB와 탐지용1바이트를 읽고 초과를 거부합니다. 명시적 UTF-8 디코딩과 중복 JSON 키 거부를 적용하며 자격 증명, 백엔드 연결, 증거 및 저장소 변경은 수행하지 않습니다.

```python #!/usr/bin/env python3 """Validate a saved `restic check --json` summary and print a read-only ledger.""" import argparse import json import re import sys from pathlib import Path PACK_ID = re.compile(r"^[0-9a-f]{64}$") def unique_object(pairs): result = {} for key, value in pairs: if key in result: raise ValueError("duplicate JSON property") result[key] = value return result def load_summary(path: Path) -> dict: if not path.is_file() or path.is_symlink(): raise ValueError("input must be a regular file, not a symlink") with path.open("rb") as stream: raw = stream.read(1024 * 1024 + 1) if len(raw) > 1024 * 1024: raise ValueError("input exceeds the 1 MiB audit limit") try: value = json.loads(raw.decode("utf-8"), object_pairs_hook=unique_object) except (UnicodeDecodeError, json.JSONDecodeError) as exc: raise ValueError("input is not one complete UTF-8 JSON document") from exc if not isinstance(value, dict) or value.get("message_type") != "summary": raise ValueError("requires the stdout summary object from restic check --json") errors = value.get("num_errors") if "broken_packs" not in value: raise ValueError("broken_packs is required, including when it is null") packs = value["broken_packs"] if packs is None: packs = [] # Restic v0.19.1 encodes an empty Go slice as JSON null. index = value.get("suggest_repair_index") prune = value.get("suggest_prune") if type(errors) is not int or errors < 0: raise ValueError("num_errors must be a nonnegative integer") if not isinstance(packs, list) or any( not isinstance(item, str) or not PACK_ID.fullmatch(item) for item in packs ): raise ValueError("broken_packs must contain complete lowercase pack IDs") if len(packs) != len(set(packs)): raise ValueError("broken_packs contains duplicates") if type(index) is not bool or type(prune) is not bool: raise ValueError("repair suggestions must be booleans") return { "numErrors": errors, "brokenPacks": packs, "suggestRepairIndex": index, "suggestPrune": prune, } def classify(summary: dict) -> dict: if summary["brokenPacks"]: status = "BROKEN_PACKS" next_step = "follow the exact restic check instruction for repair packs" elif summary["suggestRepairIndex"]: status = "INDEX_REPAIR_SUGGESTED" next_step = "follow the exact restic check instruction for repair index" elif summary["numErrors"]: status = "OTHER_ERRORS" next_step = "stop and review saved stderr or ask the restic support channel" elif summary["suggestPrune"]: status = "PRUNE_OPTIONAL_AFTER_HEALTH_REVIEW" next_step = "treat prune as cleanup, not damage repair" else: status = "NO_ERRORS_REPORTED" next_step = "retain evidence and perform a representative restore rehearsal" return { "status": status, **summary, "mutationApproved": False, "nextStep": next_step, "requiredBeforeMutation": [ "preserve full stdout, stderr, exit status and restic version", "freeze backup, forget and prune jobs", "copy the repository or at least index and snapshots when possible", "investigate storage or hardware faults before repair", ], } def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("summary", type=Path, help="saved stdout summary JSON") args = parser.parse_args() try: ledger = classify(load_summary(args.summary)) except (OSError, ValueError) as exc: print(json.dumps({"status": "INCONCLUSIVE", "reason": str(exc)})) return 2 print(json.dumps(ledger, indent=2, sort_keys=True)) return 0 if ledger["status"] == "NO_ERRORS_REPORTED" else 1 if __name__ == "__main__": raise SystemExit(main()) ```

`python3 restic_check_ledger.py check-summary.json > repair-ledger.json`을 실행합니다. 0은 `NO_ERRORS_REPORTED`만, 조치가 필요한 비-clean 분류는 1, malformed/unsupported input은 `INCONCLUSIVE`로 2입니다. redirection은 새 보고서를 만들므로 새 이름을 쓰고 원본 파일을 그대로 둡니다.

확인한 restic v0.19.1 구현은 `broken_packs`를 빈 Go 슬라이스로 초기화하며 JSON `null`로 출력할 수 있습니다. 파서는 `null`과 `[]`를 나열한 손상 팩 없음으로 해석하지만 누락된 키나 잘못된 ID는 거부합니다. 합성 테스트 19개는 다섯 상태, 널과 배열, 우선순위, 중복 키, UTF-16 거부, 제한된 읽기, 링크, 불리언, 잘못된 ID와 입력 불변인 CLI 종료0/1/2를 검사했습니다. 원래 check 종료 상태는 따로 보존하세요. 경로 경쟁용 샌드박스도 실제 저장소 진단도 아닙니다.

4. 다섯 가지 원장 결과를 해석합니다

`BROKEN_PACKS`는 summary가 완전한 damaged pack ID를 나열했음을 뜻합니다. 목록을 보존하고 같은 check/version이 지시한 `repair packs`만 사용합니다. 공식 설명상 pack repair가 index도 갱신하므로 일반 index repair보다 우선합니다.

`INDEX_REPAIR_SUGGESTED`는 check가 `suggest_repair_index`를 명시했고 summary에 broken pack이 없다는 뜻입니다. `OTHER_ERRORS`는 documented flag로 해결되지 않으므로 저장한 stderr를 보고 restic support에 문의하며 repair command를 추측하지 않습니다.

`PRUNE_OPTIONAL_AFTER_HEALTH_REVIEW`는 cleanup이지 damage repair가 아닙니다. unreferenced pack의 prune 제안은 missing needed pack과 다릅니다. `NO_ERRORS_REPORTED`는 이번 summary의 보고일 뿐 restore test나 전체 과거 coverage 증명이 아닙니다.

상태 | 입증된 내용 | 변경 경계 BROKEN_PACKS | check가 damaged pack ID를 나열 | backup과 원인 조사 후 명시된 repair-packs만 사용 INDEX_REPAIR_SUGGESTED | check가 index repair 요청 | preserved-copy gate 뒤에만 수행 OTHER_ERRORS | flag로 미해결 | 멈추고 stderr/support 검토 PRUNE_OPTIONAL_AFTER_HEALTH_REVIEW | cleanup 가능 | prune을 repair로 취급하지 않음 NO_ERRORS_REPORTED | 이번 summary에 error 없음 | 대표 restore 실행

5. metadata를 바꾸기 전에 원인을 고칩니다

반복 corruption은 배제될 때까지 storage, transport, memory, filesystem, hardware 문제로 봅니다. backend가 file을 truncate/change하는 동안 metadata만 고치면 다시 손상됩니다. backend type, restic version, 최초 storage fault를 support용으로 기록합니다.

unexpected-size pack을 무조건 수동 삭제하지 않습니다. 현재 공식 절차는 `check`를 따르고 damaged pack이면 `repair packs`를 쓰라고 합니다. 과거 forum workaround는 해당 version과 failure context용이며 보편 recipe가 아닙니다.

backup을 멈출 수 없으면 승인된 절차로 별도 repository를 초기화하고 손상 저장소는 조사용으로 offline 보존합니다. 공개 예제에 URL이나 credential을 재사용하지 말고 대체 저장소를 repair라고 부르지 않습니다.

6. check가 실제 요청한 분기만 선택합니다

damaged pack이 나열되면 저장한 check가 지정한 `repair packs` command와 ID를 씁니다. broken pack 없이 index repair가 명시된 경우에만 documented index path를 씁니다. 두 가지를 습관적으로 모두 실행하거나 healthy index를 만일에 대비해 고치지 않습니다.

index가 올바른 뒤 원래 backup task를 다시 실행하면 source에 남은 blob을 보충하고 같은 content를 참조하는 오래된 snapshot도 회복할 수 있습니다. source에서 사라진 byte는 만들지 못하며 새 snapshot 성공도 모든 오래된 snapshot의 완전성을 증명하지 않습니다.

repair 또는 backup healing 후 structural check를 다시 합니다. error가 남으면 최초 결과를 덮지 말고 새 evidence set으로 저장합니다. error set 변화는 진전 증거이지 incident history 삭제 허가가 아닙니다.

7. snapshot 자료 손실을 받기 전에 영향 파일을 매핑합니다

index가 올바르고 가능한 backup healing을 마친 뒤에만 `restic repair snapshots --dry-run`을 씁니다. 각 `would save new snapshot`을 검토하고 original snapshot, affected path, available source copy, decision owner를 기록합니다.

dry-run은 impact map이며 data를 고치거나 `--forget`을 승인하지 않습니다. 실제 repair는 다시 쓴 snapshot에서 접근 불가 content를 빼고, `--forget`은 original damaged snapshot도 지웁니다. 명시적인 data-loss decision이지 일상 cleanup이 아닙니다.

중요 path가 보이거나 dry-run이 불완전하거나 source copy를 아직 찾지 않았거나 loss 승인 권한자가 없으면 멈춥니다. 불확실하면 old snapshot을 보존하고 project support에 문의합니다.

8. check와 restore 둘 다로 복구 경로를 입증합니다

공식 closing check는 완전한 `restic check --read-data`가 error 없이 끝나는 것입니다. 크거나 비싼 backend는 window와 cost를 계획합니다. random percentage subset의 연속 실행은 전체 coverage를 보장하지 않습니다.

대표 snapshot도 새 빈 target으로 restore합니다. high-value directory, 큰 file, metadata-heavy 작은 tree를 포함하고 가능하면 application readability나 independent hash를 비교합니다. 유일한 source copy 위에 복원하지 않습니다.

final check, restore rehearsal, snapshot/path inventory, repository copy를 모두 식별한 뒤 원장을 닫고 untested 범위를 적습니다. “error 없음”과 “필요한 file 복원 가능”은 관련되지만 별도 주장입니다.

자주 묻는 질문

prune으로 error를 없앨 수 있나요? needed pack 누락이나 기타 error가 남아 있으면 안 됩니다. prune은 cleanup이며 먼저 check의 명시 repair를 따릅니다.

`repair index`가 누락 file byte를 복구하나요? 아닙니다. index만 고칩니다. source에 남은 content는 backup 재실행으로 돌아올 수 있습니다.

JSON summary가 있는데 stderr를 왜 저장하나요? 공식 error object가 stderr에 기록돼 failure detail을 보존하기 때문입니다. stable summary field로 분기해도 진단에는 원본 error가 필요합니다.

`repair snapshots --dry-run`은 파괴적인가요? dry-run은 impact review용입니다. 이후 repair는 replacement snapshot을 쓰고 `--forget`은 original을 지우므로 별도 결정입니다.

clean structural check가 모든 pack byte 완전성을 증명하나요? 아닙니다. 모든 pack 검증에는 `--read-data`, 대표 restore에는 별도 user-level recovery test가 필요합니다.

다섯 가지 저장소 check 결과를 별도 sandbox restore rehearsal 앞에 배치한 원본 개념 일러스트.
원본 개념 일러스트: check 결과가 다음 결정을 분류하고 별도 restore rehearsal이 실제 복구 가능성을 시험합니다. Watch at Ease