Minecraft 월드 백업과 격리 복원
RCON 저장, 실행 중 파일 복사, tar 압축, Cloud Storage 보존과 격리 복원을 하나로 연결한 검증 가능한 백업 계약.
운영 백업은 systemd timer가 Bash 스크립트를 실행하고, 접속자가 없을 때 RCON save-all을
호출한 뒤 world*, 설정과 플러그인을 작업 디렉터리에 복사했다. 복사본은 tar와 pigz로
압축해 Cloud Storage에 업로드했다.
원격 보관본은 남았지만 일관된 복구 지점이라는 보장은 없었다. Minecraft 쓰기를 멈추지 않은
채 cp -r를 실행했고 일부 복사 실패는 로그만 남기고 계속 진행했다. 생성 파일은
tar.gz인데 startup script의 복원 분기는 unzip을 사용했다. 장애가 실제로 발생했다는
기록이 아니라 소스에서 확인되는 복원 공백이다.
실제 백업 파이프라인과 실패 지점
flowchart LR accTitle: 운영 당시 월드 백업 파이프라인과 실패 지점 accDescr: 예약 실행 뒤 월드를 복사하고 압축해 원격에 남기는 주 경로와, 복사 일관성 및 압축 형식에서 생긴 실패 지점을 함께 표시한다. TRIGGER[systemd timer] --> CAPTURE[접속자 확인<br/>RCON save-all<br/>working copy] CAPTURE --> PACKAGE[tar + pigz] PACKAGE --> REMOTE[gsutil upload<br/>Discord 알림] CAPTURE -. 자동 저장 계속<br/>복사 오류 일부 누락 .-> CONSISTENCY[일관성·오류 전파 위험] PACKAGE -.->|tar.gz를 unzip으로 복원| FORMAT[복원 형식 불일치]
timer는 UTC 기준으로 KST 새벽에 실행됐고 놓친 실행을 다음 부팅에서 수행하도록
Persistent=true를 사용했다.
[Unit]
Description=Daily Minecraft backup
[Timer]
OnCalendar=*-*-* 19:00:00 UTC
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true는 VM이 예약 시각에 꺼져 있었다면 다음 활성화 때 놓친 작업을 실행한다.
부팅 직후 Minecraft 복원·초기화와 백업이 동시에 시작될 수 있으므로 backup service에
After=minecraft.service만 두는 것으로는 부족하다. Minecraft readiness를 확인하거나
timer에 지연을 두고, backup script 자체가 RCON 실패 시 안전하게 종료해야 한다.
중복 실행 방지는 flock으로 구현돼 있었다.
LOCKFILE=/run/lock/minecraft-backup.lock
exec 200>"$LOCKFILE"
flock -n 200 || {
echo "backup already running" >&2
exit 1
}
단일 VM의 timer와 수동 실행 사이에는 충분한 경계다. lock 파일을 지우는 것으로 실행 중 lock을 해제하지 말고 file descriptor가 닫힐 때 커널이 lock을 반환하게 둔다.
save-all만으로 시점이 고정되지 않는 이유
Minecraft의 최신 상태는 JVM 메모리와 여러 디스크 파일에 나뉜다. region, entity,
playerdata와 level 파일은 같은 순간에 한 번에 교체되지 않는다. save-all은 메모리 상태를
디스크로 쓰게 하지만 그 뒤의 자동 저장까지 멈추지 않는다.
실제 핵심 코드는 다음 순서였다.
execute_rcon_command "save-all"
cp -r "$MINECRAFT_DIR/world"* "$TMP_BACKUP_DIR/" \
2>/dev/null || log_error "world copy failed"
cp "$MINECRAFT_DIR/"{ops.json,whitelist.json,server.properties} \
"$TMP_BACKUP_DIR/" 2>/dev/null || log_error "config copy failed"
tar -I 'pigz -9' -cf "$BACKUP_PATH" -C "$TMP_BACKUP_DIR" .
두 cp 실패가 exit로 이어지지 않아 필수 월드가 빠진 archive도 압축·업로드될 수 있다.
또한 큰 월드를 복사하는 동안 자동 저장이 일어나면 먼저 복사한 region과 나중에 복사한
playerdata의 기준 시각이 달라질 수 있다. cp -r 결과를 스냅숏이라 부르지 않고 작업
복사본이라 부르는 이유다.
접속자가 없다는 조건도 쓰기 중지를 뜻하지 않는다. 서버 tick, 플러그인, 자동 저장과 scheduled task가 계속 파일을 바꿀 수 있다. 접속자 확인은 체감 부하를 줄이는 운영 정책이고, 일관성은 별도 저장 구간으로 만든다.
실패에도 닫히는 쓰기 중지 구간
작은 서버에서는 save-off, save-all flush, 복사, save-on 순서로 작업 복사본의 기준을
고정할 수 있다.
sequenceDiagram
accTitle: application-consistent 작업 복사본 생성 시퀀스
accDescr: 백업 작업이 자동 저장을 끄고 모든 상태를 디스크에 flush한 뒤 파일을 복사하며 성공과 실패 모두에서 자동 저장을 다시 켠다.
participant Job as backup job
participant MC as Minecraft
participant Disk as live world
participant Work as working copy
Job->>MC: save-off
Job->>MC: save-all flush
MC-->>Job: 저장 완료 응답
Job->>Disk: 필수 파일 목록 검증
Disk->>Work: copy
alt 복사 성공
Job->>MC: save-on
Job->>Work: manifest·checksum
else 명령·복사 실패
Job->>MC: save-on
Job-->>Job: 실패 종료·업로드 금지
end
save-on은 성공 경로의 마지막 줄이 아니라 cleanup handler에 둔다. Bash에서는 쓰기 중지
여부를 상태로 기록해 불필요한 save-on 호출도 피할 수 있다.
set -Eeuo pipefail
save_disabled=0
work_dir=$(mktemp -d /backup/minecraft.XXXXXX)
cleanup() {
status=$?
if (( save_disabled == 1 )); then
rcon "save-on" || echo "CRITICAL: save-on failed" >&2
fi
rm -rf -- "$work_dir"
exit "$status"
}
trap cleanup EXIT INT TERM
rcon "save-off"
save_disabled=1
rcon "save-all flush"
cp -a -- /opt/minecraft/world /opt/minecraft/world_nether \
/opt/minecraft/world_the_end "$work_dir/"
rcon "save-on"
save_disabled=0
set -e만으로 모든 실패가 잡히지는 않지만, 필수 복사에 || log만 붙여 성공으로 바꾸는
패턴은 피할 수 있다. 선택 파일과 필수 파일을 목록으로 분리하고 필수 파일이 하나라도 없으면
archive를 만들지 않는다. cleanup의 save-on 실패는 게임 상태를 위험하게 하므로 일반 백업
실패보다 높은 우선순위로 알린다.
복사 시간 동안 자동 저장이 멈추므로 월드가 커질수록 플레이 중단 위험도 커진다. 별도 Persistent Disk나 filesystem snapshot을 사용하더라도 Minecraft가 먼저 flush된 상태여야 application-consistent 지점에 가까워진다. 디스크 snapshot만 바로 찍으면 VM crash 뒤와 비슷한 crash-consistent 상태일 수 있다.
작업 복사본의 검증 가능한 archive
Minecraft 쓰기를 다시 켠 뒤 작업 복사본을 압축하면 긴 압축 시간을 저장 중지 구간에서 제외할 수 있다. archive 생성 전후에는 용량과 파일 목록을 확인한다.
archive="minecraft-$(date -u +%Y%m%dT%H%M%SZ).tar.gz"
tar --create --gzip --file "$archive" \
--directory "$work_dir" \
world world_nether world_the_end server.properties plugins
sha256sum "$archive" > "$archive.sha256"
tar --list --gzip --file "$archive" >/dev/null
실제 코드는 tar -I 'pigz -9'를 사용했다. 파일 확장자와 해제 도구의 계약은 압축 구현과
일치해야 한다. tar.gz는 tar -xzf 또는 동등한 tar API로 풀고 ZIP일 때만 unzip을
사용한다.
archive 옆 manifest에는 복원 판단에 필요한 최소 정보를 둔다.
{
"schema_version": 1,
"archive_format": "tar.gz",
"created_at": "2026-07-23T00:00:00Z",
"server_version": "example-version",
"worlds": ["world", "world_nether", "world_the_end"],
"sha256": "example-sha256",
"uncompressed_bytes": 123456789
}
실제 project, bucket, 사용자 이름, RCON 자격정보는 manifest에 넣지 않는다. 플러그인이 월드 밖에 별도 데이터베이스를 사용한다면 그 백업과 일관성 지점도 manifest에 포함해야 한다. 파일 플러그인만 있다고 가정하고 데이터베이스를 누락하면 Minecraft 월드는 열려도 운영 상태가 복원되지 않을 수 있다.
업로드 완료와 보관 정책
Cloud Storage 업로드는 archive와 checksum·manifest가 모두 성공해야 완료다. 임시 객체 이름으로 올린 뒤 검증하고 최종 prefix로 복사하거나, manifest를 마지막에 올려 manifest가 존재하는 묶음만 완성본으로 취급할 수 있다. Discord 알림은 완료된 object generation과 검증 결과를 요약하고 signed URL이나 secret을 로그에 남기지 않는다.
운영 당시 오래된 백업은 Cloud Scheduler가 Pub/Sub을 발행하고 별도 함수가 객체를 삭제했다. 기간만 기준으로 삭제한다면 Cloud Storage lifecycle rule이 더 적은 리소스와 권한으로 같은 정책을 표현한다.
resource "google_storage_bucket" "backup" {
name = "example-minecraft-backups"
location = var.region
uniform_bucket_level_access = true
versioning {
enabled = true
}
lifecycle_rule {
condition {
age = 30
matches_prefix = ["verified/"]
}
action {
type = "Delete"
}
}
}
단순 age 정책은 “최근에 복원 시험을 통과한 월별 백업은 오래 보존한다” 같은 의미를 알지 못한다. 검증 상태, legal hold, 월별·연별 보존처럼 객체별 판단이 필요할 때만 정리 함수와 상태 저장소를 유지한다. 어떤 방식이든 운영 VM 서비스 계정에는 해당 bucket prefix의 객체 생성과 조회만 주고 bucket 설정 변경·전체 삭제 권한은 분리한다.
별도 상태 머신으로 분리한 복원
백업 다운로드가 끝났다고 운영 월드에 덮어쓰지 않는다. 격리 경로에서 무결성과 서버 기동을 먼저 확인한다.
stateDiagram-v2 accTitle: Minecraft 백업 격리 복원 상태 머신 accDescr: 백업 선택 뒤 manifest, checksum, 압축과 파일 구조를 검사하고 격리 서버를 기동해 월드를 확인한 뒤에만 운영 교체 후보가 된다. [*] --> SELECTED: backup 선택 SELECTED --> MANIFEST_OK: schema·format 검사 MANIFEST_OK --> CHECKSUM_OK: SHA-256 일치 CHECKSUM_OK --> EXTRACTED: 격리 디렉터리에 해제 EXTRACTED --> STRUCTURE_OK: 필수 world·설정 검사 STRUCTURE_OK --> BOOTED: 별도 포트로 서버 기동 BOOTED --> VERIFIED: protocol·월드·플러그인 표본 검사 VERIFIED --> CANDIDATE: 운영 교체 후보 SELECTED --> FAILED: 검증 실패 MANIFEST_OK --> FAILED CHECKSUM_OK --> FAILED EXTRACTED --> FAILED STRUCTURE_OK --> FAILED BOOTED --> FAILED
복원 작업은 운영 서버와 다른 경로·포트, 가능하면 임시 VM에서 수행한다. 네트워크를 공개하지 않고 동일 Java와 서버 버전을 사용한다. 서버 준비 로그, Java protocol status, 월드 seed와 대표 좌표, 플레이어 데이터, whitelist와 핵심 플러그인 상태를 표본 검사한다.
운영 교체가 필요하면 기존 디렉터리를 즉시 삭제하지 않는다. Minecraft를 정상 종료하고 현재 디렉터리를 rollback 가능한 이름으로 이동한 뒤 검증된 복원 디렉터리를 같은 filesystem 안에서 rename한다. 소유권과 SELinux/AppArmor 조건을 확인하고 기동한다. 실패하면 복원본을 격리하고 기존 디렉터리로 되돌린다.
실제 복원으로 계산하는 RPO와 RTO
RPO는 백업 예약 주기가 아니라 복원 가능한 가장 최근 시점과 장애 시점의 차이다. 매일 백업해도 최근 3개가 모두 무결성 검사를 통과하지 못했다면 실제 RPO는 더 길다. RTO는 파일 다운로드만이 아니라 선택, 검증, 해제, 서버 기동, 월드 확인과 전환까지 포함한다.
flowchart LR accTitle: 백업 보존과 복원 검증 선택 accDescr: 생성된 백업은 checksum 검사를 거쳐 verified prefix로 이동하고 정기 격리 복원을 통과한 보관본만 운영 복구 기준이 된다. NEW[새 archive] --> HASH[checksum·manifest 검사] HASH -- 실패 --> BAD[격리·경보] HASH -- 성공 --> VERIFIED[verified prefix] VERIFIED --> RESTORE[정기 격리 복원] RESTORE -- 실패 --> BAD RESTORE -- 성공 --> PROVEN[복구 가능 백업] PROVEN --> LIFE[보존 정책]
복원 기록에는 backup object generation, 생성 시각, 검증 시각, 서버 버전, 복원 소요 시간, 검사 결과와 작업자를 남긴다. 운영자가 의지할 수 있는 기준은 “파일이 존재함”이 아니라 “어느 보관본을 언제 실제 서버로 열어 봤는가”다.
참고
Share
아직 댓글이 없습니다. 첫 댓글을 남겨주세요.
검토 대기 중