별도 위치에 복원
90분 안팎
학습 목표
제공된 실습 파일만 삭제한 뒤 별도 restore 경로에 복원해 내용·소유자·권한을 비교합니다.
개념
복원 위치를 먼저 정하는 이유
복원 작업은 백업을 읽는 작업인 동시에 새 파일을 쓰는 작업입니다. 기존 서비스 경로에 바로 풀면 잘못된 파일이 현재 데이터와 섞이고 복구 전 상태를 잃을 수 있습니다. 이번 레슨은 source와 다른 새 restore 경로에만 파일을 생성합니다. 원본 삭제 연습도 제공 테스트의 임시 fixture에 한정합니다. 실제 VM의 data는 보존한 채 복원 결과를 별도 경로에서 비교하므로 검증에 실패해도 원본은 남습니다.
새 경로라는 조건을 검사합니다
restore의 destination은 아직 존재하지 않아야 하고 source의 내부·동일 경로·상위 경로이면 거부합니다. 단순 문자열 startswith로 비교하면 lab과 lab-old를 혼동할 수 있으므로 해석한 경로의 부모 관계로 판정합니다. 파일과 조상 디렉터리에 심볼릭 링크가 있으면 거부합니다. destination must be absent 오류는 폴더가 이미 있다는 뜻이며 빈 폴더라도 이번 구현에서는 새 이름을 선택합니다.
압축을 풀기 전에 전체를 읽어 검사합니다
외부 SHA-256 값과 묶음 바이트를 비교한 뒤 같은 바이트를 메모리에서 열어 내부 목록을 검사합니다. 검사할 때 읽은 파일과 풀 때 다시 읽은 파일이 바뀌는 문제를 줄이기 위한 순서입니다. 큰 백업을 메모리에 통째로 읽는 운영 구현은 아니며 파일별 1MiB라는 실습 상한을 적용합니다. 기준 해시가 없는 묶음을 그냥 풀지 않고 체크섬 파일까지 있는지 먼저 확인합니다.
압축 멤버 이름은 신뢰하지 않습니다
압축 안에 ../escape나 절대 경로, 링크·장치 파일이 있으면 다른 위치를 건드릴 수 있습니다. 실습 구현은 정확한 다섯 파일과 manifest.json 이름만 허용하고 중복 이름·일반 파일 아닌 종류·크기 초과를 거부합니다. extractall로 임의 멤버를 그대로 쓰지 않고 검사된 바이트를 정한 상대 경로에 직접 씁니다. 단순히 최상위 폴더가 하나라는 이유로 모든 아카이브가 안전하다고 판단하지 않습니다.
체크섬 오류에서 정상 값을 다시 만들지 않습니다
archive checksum mismatch가 나오면 기준과 읽은 묶음이 다릅니다. 전달 과정의 손상인지 저장 문제인지 원본 백업 위치의 값과 관측을 확인합니다. 실패한 파일에서 새 해시를 계산해 기준 파일에 쓰면 손상을 승인하는 셈이 됩니다. 다음 유효 백업을 선택할 때 데이터 시점이 더 오래되어 RPO 목표를 넘는지도 기록합니다. 오류를 없애는 일보다 복원 가능한 기준을 찾는 일이 먼저입니다.
파일별 내용도 묶음 기준과 비교합니다
manifest의 각 sha256과 읽은 바이트를 대조합니다. 묶음 전체가 전달 중 그대로였어도 백업 생성 시 잘못된 내부 명세가 있으면 member checksum mismatch로 끝납니다. 필수 파일이나 내부 명세가 빠진 경우 incomplete archive로 거부합니다. 이 검사는 파일이 읽힌다는 사실과 복구 대상이 완전하다는 사실을 나눕니다. 기대 목록은 앞 레슨에서 정한 복구 단위를 그대로 사용합니다.
임시 경로에서 완성한 뒤 게시합니다
모든 읽기 검사가 끝나면 destination의 부모 아래에 .restore-로 시작하는 임시 디렉터리를 만듭니다. 검증된 파일 바이트를 쓰고 모드를 적용한 다음 manifest를 보관합니다. 작업을 마친 디렉터리 이름을 destination으로 바꿔 소비자가 반쯤 복원된 결과를 보지 않게 합니다. 예외가 나면 이 함수가 만든 임시 경로만 정리합니다. 부모 경로와 destination은 다른 작성자가 바꾸지 않는 전용 실습 영역이어야 합니다.
내용이 같아도 권한이 다를 수 있습니다
SHA-256 결과가 같지만 파일이 600에서 644로 바뀌면 다른 사용자가 읽을 수 있습니다. 해시와 모드·UID/GID를 별도로 검사하는 이유입니다. 복원 함수는 파일의 모드를 적용하되 일반 사용자가 임의 소유자를 복구할 수 있다고 가정하지 않습니다. 로컬 fixture에서는 같은 사용자로 만들고 복원해 계정 검사가 가능합니다. 실제 VM은 목록의 계정 이름과 숫자 ID 매핑을 먼저 확인한 뒤 필요한 chown만 수행합니다.
다른 호스트의 숫자 ID를 그대로 믿지 않습니다
UID 1001이 원본에서 bcweb이어도 다른 VM에서는 다른 사용자일 수 있습니다. manifest에는 이름과 숫자를 같이 남기며 VM 검사는 두 값이 기존 계정 정보와 일치할 때만 진행합니다. 이 모듈은 동일 실습 VM의 계정을 유지하므로 별도 계정 생성이나 다른 호스트로의 소유권 매핑 자동화는 하지 않습니다. restored owner mismatch가 나오면 root로 실행해 감추기보다 기록과 실제 계정을 비교합니다.
상위 디렉터리도 서비스가 통과해야 합니다
파일 자체가 bcweb 소유이고 읽기 가능해도 상위 폴더에 탐색 권한이 없으면 서비스가 접근하지 못합니다. VM 복원 경로는 bcapp 그룹과 필요한 750 탐색 권한을 적용하고 data·config에도 같은 접근 경계를 둡니다. 파일 모드는 백업의 값을 유지합니다. 권한 오류를 해결하려고 복원 폴더 전체를 777로 바꾸면 검증할 정책이 사라집니다. 계정·그룹·파일·부모 경로를 각각 확인합니다.
제공 데이터 삭제 연습은 사본에서만 합니다
demo.py restore는 새 임시 source의 items.json 하나를 unlink한 뒤 백업을 별도 restore로 되살립니다. SOURCE_EXISTS False는 그 사본 파일이 여전히 없다는 뜻이며 RESTORED_COUNT 2는 새 경로에서 데이터가 돌아왔다는 뜻입니다. 복원이 원본에 다시 쓴 결과가 아니라는 것을 함께 확인합니다. 출력의 MODE 0o640은 fixture에 정한 권한이며 실제 VM의 업무 파일 모드를 고정하는 지시는 아닙니다.
손상 사례에서는 쓰기를 시작하지 않습니다
검사기는 바깥 해시가 다른 묶음, 경로 이탈 이름, 링크 멤버와 기존 목적지에 대해 복원을 거부하는지 봅니다. 실패 뒤 destination이 없고 source 파일이 그대로 있어야 합니다. 내부 해시나 권한을 잘못 처리하는 구현은 정상 데이터 사례만으로 잡기 어려우므로 경계 검사를 함께 둡니다. 복원 실패의 종료 코드를 무시하고 다음 서비스 기동으로 넘어가지 않습니다.
복원 파일의 사후 변경도 잡습니다
verify_restored는 복원 경로에서 다섯 파일을 다시 읽어 해시와 실제 모드·소유자를 비교하고 items 전체 JSON을 확인합니다. 복원 후 app.py의 모드만 바꾸거나 items.json을 비워도 실패해야 합니다. source의 최신 값과 비교하는 대신 선택한 백업의 expected_items를 기준으로 합니다. 백업 이후 데이터가 늘었다면 현재 source보다 적은 값이 정상 복원일 수 있고 그 차이는 유실 범위 기록으로 따로 남깁니다.
목적지 검증을 끝내고 다음 단계로 넘깁니다
RestoreTest는 여섯 백업 회귀와 여섯 복원 검사를 함께 실행합니다. starter에서 미완성 restore를 구현하고 열두 검사 모두 통과시킵니다. 완료 증거에는 백업 식별자·복원 경로·파일별 내용·계정·모드와 원본 보존 결과를 담습니다. sha256sum의 체크 파일 경로 규칙은 더 읽기를 참고하며 다음 레슨은 이 새 경로를 실제 실행 계정으로 열어 HTTP 업무 응답까지 확인합니다.
따라하기
사본 하나만 삭제하고 별도 복원
solution ZIP에서 실행합니다. 삭제되는 것은 demo가 만든 임시 items.json이며 실제 /srv 데이터는 보존합니다.
python3 -B demo.py restore실행 결과
SOURCE_EXISTS False RESTORED_COUNT 2 MODE 0o640
해시만으로 모드를 판단할 수 없음 확인
한 임시 파일의 바이트를 유지하며 모드를 바꿉니다. 백업 해시와 모드는 각각 확인해야 합니다.
import hashlib, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
p = Path(d).resolve() / "items.json"
p.write_bytes(b"fixture\n"); p.chmod(0o600)
old = hashlib.sha256(p.read_bytes()).hexdigest()
p.chmod(0o644)
print("HASH_SAME", old == hashlib.sha256(p.read_bytes()).hexdigest())
print("MODE", oct(p.stat().st_mode & 0o777))실행 결과
HASH_SAME True MODE 0o644
경로 부모 관계로 중첩 구분
문자열 접두사가 아닌 경로 요소로 원본 내부 여부를 봅니다.
from pathlib import PurePosixPath
source = PurePosixPath("/srv/lab")
for text in ("/srv/lab/restore", "/srv/lab-old/restore"):
target = PurePosixPath(text)
print(text, source == target or source in target.parents)실행 결과
/srv/lab/restore True /srv/lab-old/restore False
복원 구현을 열두 검사로 확인
starter의 restore를 완성하고 실행합니다. solution은 stderr에 Ran 12 tests와 OK를 표시합니다. failed 검사의 목적지·해시·모드·ID 조건을 구분해 수정합니다.
bash check.sh확인 문제
실습
recovery.py의 restore TODO에 원본·목적지 중첩 거부 조건을 완성합니다. 같은 경로, 원본 내부, 원본의 상위 경로를 모두 거부합니다. 나머지 구현을 따라 inspect 검증, 임시 폴더의 내용·모드 적용, 새 이름 게시, 실패 시 자기 임시 경로 정리를 설명합니다. 테스트는 제공 데이터 삭제 후 별도 복원·손상·경로 이탈·링크·계정·모드·원본 보존을 확인합니다.
실행 명령
bash check.sh
기대 결과
12 tests, OK, 종료 코드 0
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 백업 결과를 신뢰하기 위한 확인 방법을 설명합니다.