도움말:S3 연구 메모리/백업과 복원: 두 판 사이의 차이
S3 연구 메모리 저장소 문서 게시 |
S3 연구 메모리 저장소 문서 동기화 |
||
| (같은 사용자의 중간 판 8개는 보이지 않습니다) | |||
| 14번째 줄: | 14번째 줄: | ||
|- | |- | ||
| <code><nowiki>db_data</nowiki></code> || MediaWiki 페이지, 의미 데이터, 사용자, 변경 이력, 로그, 호환 필드 || 기준 데이터베이스 | | <code><nowiki>db_data</nowiki></code> || MediaWiki 페이지, 의미 데이터, 사용자, 변경 이력, 로그, 호환 필드 || 기준 데이터베이스 | ||
|- | |||
| <code><nowiki>db_binlogs</nowiki></code> || MariaDB ROW binary log와 index || 기준 덤프 이후 시간점 복구(PITR). 기본 14일 보존 | |||
|- | |- | ||
| <code><nowiki>mediawiki_images</nowiki></code> || 업로드한 근거 파일과 MediaWiki 업로드 메타데이터 || DB 덤프와 같은 시점이어야 함 | | <code><nowiki>mediawiki_images</nowiki></code> || 업로드한 근거 파일과 MediaWiki 업로드 메타데이터 || DB 덤프와 같은 시점이어야 함 | ||
| 22번째 줄: | 24번째 줄: | ||
|- | |- | ||
| <code><nowiki>caddy_config</nowiki></code> || Caddy 실행 설정 || 저장소와 공개 hostname으로 다시 생성 가능 | | <code><nowiki>caddy_config</nowiki></code> || Caddy 실행 설정 || 저장소와 공개 hostname으로 다시 생성 가능 | ||
|- | |||
| <code><nowiki>embedding_models</nowiki></code> || SHA-256으로 확인한 번들 GGUF || 다시 받을 수 있는 파생 캐시. Lesson 복구 세트가 아님 | |||
|} | |} | ||
백업 스크립트는 | 일반 백업 스크립트는 DB 덤프, 설정, 업로드와 함께 <code><nowiki>db_binlogs</nowiki></code>의 캡처 시점 구간과 덤프에 내장된 replay 시작 file·position을 저장합니다. 이 항목이 Lesson 복구 세트입니다. 새 <code><nowiki>db_binlogs</nowiki></code> 볼륨은 one-shot <code><nowiki>db-binlog-init</nowiki></code>이 MariaDB 계정만 쓸 수 있도록 소유권과 mode를 맞춘 뒤 DB가 시작합니다. 이 서비스가 실패하면 DB를 시작하지 않으며, 볼륨 권한을 넓혀 우회하지 않습니다. <code><nowiki>caddy_data</nowiki></code>와 <code><nowiki>caddy_config</nowiki></code>는 평소 재시작 때 남겨 두되, 버전이 있는 위키 백업에 섞지 않습니다. 이 볼륨이 없어지면 DNS와 외부 80/443이 정상일 때 Caddy가 인증서를 다시 받을 수 있습니다. 일반 운영에서는 <code><nowiki>docker compose down --volumes</nowiki></code>를 쓰지 마세요. | ||
<code><nowiki>embedding_models</nowiki></code>에는 Lesson, 검색 query나 벡터가 없습니다. 백업·복원 스크립트는 이 볼륨을 포함하지 않으며, 의미 검색을 다시 켤 때 고정된 원본과 SHA-256으로 재구성합니다. 평소에는 재다운로드와 서비스 중단을 피하도록 볼륨을 남겨 둡니다. | |||
저장소에는 다시 만들 수 있는 코드, 템플릿, 양식이 있습니다. 자격 증명이 든 <code><nowiki>.env</nowiki></code>는 추적하지 않으며 일반 백업 아카이브에도 넣지 않습니다. 서버 담당자가 복구용 사본을 따로 보관하되 Git이나 공개 아카이브에는 넣지 마세요. | 저장소에는 다시 만들 수 있는 코드, 템플릿, 양식이 있습니다. 자격 증명이 든 <code><nowiki>.env</nowiki></code>는 추적하지 않으며 일반 백업 아카이브에도 넣지 않습니다. 서버 담당자가 복구용 사본을 따로 보관하되 Git이나 공개 아카이브에는 넣지 마세요. | ||
복구 세트의 <code><nowiki>runtime-images.txt</nowiki></code>는 Compose에 설정한 image reference와 실제 컨테이너 image ID를 함께 기록합니다. <code><nowiki>repository-tracked.patch.gz</nowiki></code>는 현재 HEAD 기준의 '''추적된 파일''' 차이만 담습니다. <code><nowiki>.env</nowiki></code>, untracked dump, upload, 자격 증명은 구조적으로 제외됩니다. untracked 파일이 있었다면 metadata에 <code><nowiki>present_not_archived</nowiki></code>만 남고 내용과 이름은 보관하지 않습니다. root systemd timer도 이 checkout의 정규화된 경로만 Git 명령별 <code><nowiki>safe.directory</nowiki></code>로 허용하므로 전역 Git 신뢰 설정을 바꾸지 않으면서 같은 source metadata와 patch를 만듭니다. | |||
서버 <code><nowiki>.env</nowiki></code>에는 MCP 쓰기 토큰의 해시만 있습니다. 전달용 원문 토큰이 든 mode <code><nowiki>0600</nowiki></code> <code><nowiki>~/.config/s3-research-memory/client.env</nowiki></code>도 최소 한 곳에 보관하세요. 원문 토큰을 모두 잃었다면 <code><nowiki>./scripts/configure-mcp-token.sh --rotate</nowiki></code>로 새 토큰을 만들고 쓰기 클라이언트에 다시 전달합니다. 이 작업 중에도 공개 조회는 계속됩니다. | 서버 <code><nowiki>.env</nowiki></code>에는 MCP 쓰기 토큰의 해시만 있습니다. 전달용 원문 토큰이 든 mode <code><nowiki>0600</nowiki></code> <code><nowiki>~/.config/s3-research-memory/client.env</nowiki></code>도 최소 한 곳에 보관하세요. 원문 토큰을 모두 잃었다면 <code><nowiki>./scripts/configure-mcp-token.sh --rotate</nowiki></code>로 새 토큰을 만들고 쓰기 클라이언트에 다시 전달합니다. 이 작업 중에도 공개 조회는 계속됩니다. | ||
| 41번째 줄: | 49번째 줄: | ||
</nowiki></pre> | </nowiki></pre> | ||
먼저 <code><nowiki>mcp-http</nowiki></code>를 멈추면 에이전트 변경 경로가 닫힙니다. 백업 스크립트는 DB를 기다린 뒤 <code><nowiki>mediawiki</nowiki></code>와 실행 중인 <code><nowiki>bootstrap</nowiki></code>을 멈춰 | 먼저 <code><nowiki>mcp-http</nowiki></code>를 멈추면 에이전트 변경 경로가 닫힙니다. 백업 스크립트는 DB를 기다린 뒤 <code><nowiki>mediawiki-jobs</nowiki></code>, <code><nowiki>mediawiki</nowiki></code>와 실행 중인 <code><nowiki>bootstrap</nowiki></code>을 멈춰 브라우저와 job 변경도 막습니다. MariaDB binary log를 교체해 file·position을 고정한 뒤 <code><nowiki>--master-data=2</nowiki></code> 논리 덤프, binary-log 구간, 업로드·설정 볼륨을 묶습니다. 백업과 복구 스크립트는 같은 checkout 디렉터리에 nonblocking maintenance lock을 겁니다. 예약 백업, 수동 백업, 복구 중 둘 이상을 겹쳐 실행하면 나중 시작한 작업이 즉시 실패합니다. 다른 checkout으로 같은 운영 Compose project를 동시에 관리하지 마세요. 백업 종료 때는 <code><nowiki>mediawiki</nowiki></code>, 실행 중이던 <code><nowiki>bootstrap</nowiki></code>, <code><nowiki>mediawiki-jobs</nowiki></code> 순서로 복원하고 job worker health를 확인한 뒤에만 <code><nowiki>mcp-http</nowiki></code>를 다시 엽니다. worker가 준비되지 않으면 백업 파일 생성 여부와 관계없이 실패로 끝나며 MCP는 중지 상태를 유지합니다. 실패해도 exit trap이 스크립트가 관리한 서비스를 백업 전 상태로 돌립니다. <code><nowiki>mcp-http</nowiki></code>는 명시적으로 멈췄으므로 성공하거나 원인을 확인한 실패 뒤에도 마지막 <code><nowiki>up</nowiki></code> 명령을 실행해야 합니다. | ||
모든 파일의 형식과 checksum을 확인한 뒤에만 시각이 붙은 최종 디렉터리를 공개합니다. 덤프나 근거 아카이브를 저장소에 넣지 않습니다. 끝에 출력되는 복구 디렉터리의 정확한 경로를 기록하세요. | 모든 파일의 형식과 checksum을 확인한 뒤에만 시각이 붙은 최종 디렉터리를 공개합니다. 덤프나 근거 아카이브를 저장소에 넣지 않습니다. 끝에 출력되는 복구 디렉터리의 정확한 경로를 기록하세요. | ||
==== binary log를 처음 켜는 서버 ==== | |||
이 설정을 배포하기 전부터 실행 중인 DB는 컨테이너를 다시 만들기 전까지 <code><nowiki>log_bin=OFF</nowiki></code>일 수 있습니다. 이 상태에서 일반 백업은 중단되며, 최초 이전 전에만 다음 명시적 기준 백업을 사용합니다. | |||
<pre><nowiki> | |||
./scripts/backup.sh --legacy-base /srv/s3rm-backups | |||
</nowiki></pre> | |||
<code><nowiki>--legacy-base</nowiki></code>는 <code><nowiki>log_bin=OFF</nowiki></code>인 DB에서만 실행됩니다. <code><nowiki>database.sql.gz</nowiki></code>, 두 볼륨 아카이브, <code><nowiki>metadata.txt</nowiki></code>, <code><nowiki>SHA256SUMS</nowiki></code>로 이루어진 복원 가능한 format 1 기준 백업이며 PITR 보조 파일은 '''하나도''' 생성하지 않습니다. 나중에 별도 <code><nowiki>database-binlogs.tar.gz</nowiki></code>나 <code><nowiki>AUXILIARY-SHA256SUMS</nowiki></code>를 더해 일반 백업처럼 만들지 마세요. 보조 파일이 일부만 있는 세트는 복원과 격리 검증이 거부합니다. | |||
체크섬을 확인하고 완성된 디렉터리를 원격 저장소에 복사한 뒤에만 DB를 재생성합니다. <code><nowiki>db_data</nowiki></code>는 지우지 않고 새 <code><nowiki>db_binlogs</nowiki></code> 볼륨을 연결합니다. | |||
<pre><nowiki> | |||
docker compose stop mediawiki-jobs mcp-http bootstrap mediawiki | |||
docker compose up -d --force-recreate --wait db | |||
docker compose exec -T db sh -eu -c ' | |||
MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb \ | |||
--host=127.0.0.1 --user=root --batch \ | |||
--execute="SHOW VARIABLES LIKE '\''log_bin'\''; SHOW MASTER STATUS;" | |||
' | |||
make domain-up | |||
./scripts/backup.sh /srv/s3rm-backups | |||
</nowiki></pre> | |||
출력에 <code><nowiki>log_bin\tON</nowiki></code>과 <code><nowiki>mariadb-bin.NNNNNN</nowiki></code> 행이 모두 있어야 합니다. 마지막 일반 백업은 PITR 보조 체인까지 갖춘 새 기준입니다. 이후에는 <code><nowiki>--legacy-base</nowiki></code>를 사용하지 마세요. | |||
복구 디렉터리는 다음 파일을 한 세트로 가집니다. | 복구 디렉터리는 다음 파일을 한 세트로 가집니다. | ||
| 51번째 줄: | 85번째 줄: | ||
|- | |- | ||
| <code><nowiki>database.sql.gz</nowiki></code> || 설정된 MediaWiki DB 전체 논리 덤프 | | <code><nowiki>database.sql.gz</nowiki></code> || 설정된 MediaWiki DB 전체 논리 덤프 | ||
|- | |||
| <code><nowiki>database-binlogs.tar.gz</nowiki></code> || 일반 백업만: 캡처 시점까지 보존된 MariaDB ROW binary log·index 보조 아카이브 | |||
|- | |||
| <code><nowiki>pitr-manifest.txt</nowiki></code> || 일반 백업만: 기준 dump 뒤 replay를 시작할 binary-log file·position·GTID, archive 목록·digest | |||
|- | |- | ||
| <code><nowiki>mediawiki-images.tar.gz</nowiki></code> || 업로드·근거 볼륨의 numeric-owner 아카이브 | | <code><nowiki>mediawiki-images.tar.gz</nowiki></code> || 업로드·근거 볼륨의 numeric-owner 아카이브 | ||
| 56번째 줄: | 94번째 줄: | ||
| <code><nowiki>mediawiki-config.tar.gz</nowiki></code> || 생성된 설정과 위키 키의 numeric-owner 아카이브 | | <code><nowiki>mediawiki-config.tar.gz</nowiki></code> || 생성된 설정과 위키 키의 numeric-owner 아카이브 | ||
|- | |- | ||
| <code><nowiki>metadata.txt</nowiki></code> || 형식 버전, DB 이름, 페이지·revision 수, 소스 revision, 이미지 참조, Compose | | <code><nowiki>runtime-images.txt</nowiki></code> || 일반 백업만: 설정 reference와 실제 실행 컨테이너 image ID | ||
|- | |||
| <code><nowiki>repository-tracked.patch.gz</nowiki></code> || 일반 백업만: HEAD에 적용할 추적 소스 binary patch. untracked·ignored 파일 제외 | |||
|- | |||
| <code><nowiki>repository-tracked-manifest.txt</nowiki></code> || 일반 백업만: 추적 파일별 SHA-256과 소스 상태 | |||
|- | |||
| <code><nowiki>metadata.txt</nowiki></code> || 형식 버전, DB 이름, 페이지·revision 수, 소스 revision, 이미지 참조, Compose 해시. 일반 백업은 PITR position도 포함 | |||
|- | |- | ||
| <code><nowiki>SHA256SUMS</nowiki></code> || 앞의 네 payload checksum | | <code><nowiki>SHA256SUMS</nowiki></code> || 앞의 네 payload checksum | ||
|- | |||
| <code><nowiki>AUXILIARY-SHA256SUMS</nowiki></code> || 일반 백업만: PITR·image·tracked-source 보조 파일 checksum. 이 파일 digest는 <code><nowiki>metadata.txt</nowiki></code>를 통해 기본 manifest와 결합 | |||
|} | |} | ||
| 77번째 줄: | 123번째 줄: | ||
스크립트가 실패하면 그 백업은 사용할 수 없습니다. 성공한 뒤에도 다음을 확인합니다. | 스크립트가 실패하면 그 백업은 사용할 수 없습니다. 성공한 뒤에도 다음을 확인합니다. | ||
# DB 덤프, 설정·이미지 아카이브, 메타데이터, | # DB 덤프, 설정·이미지 아카이브, 메타데이터, <code><nowiki>SHA256SUMS</nowiki></code>가 있는지 확인합니다. 일반 백업은 binary-log·PITR 보조 파일 전체와 <code><nowiki>AUXILIARY-SHA256SUMS</nowiki></code>도 있어야 합니다. 최초 <code><nowiki>--legacy-base</nowiki></code>는 보조 파일이 하나도 없어야 합니다. | ||
# 스크립트가 출력한 checksum 명령을 실행합니다. ```bash (cd /ABS/PATH/TO/BACKUP-DIRECTORY && \ sha256sum --check --strict SHA256SUMS) ``` | # 스크립트가 출력한 checksum 명령을 실행합니다. ```bash (cd /ABS/PATH/TO/BACKUP-DIRECTORY && \ sha256sum --check --strict SHA256SUMS) # 일반 PITR 백업에서만 추가 확인 (cd /ABS/PATH/TO/BACKUP-DIRECTORY && \ sha256sum --check --strict AUXILIARY-SHA256SUMS) ``` | ||
# 빈 파일이 없는지, 시각과 소스 버전이 맞는지 확인합니다. | # 빈 파일이 없는지, 시각과 소스 버전이 맞는지 확인합니다. | ||
# 디렉터리 전체를 한 단위로 복사합니다. 서로 다른 시각의 파일을 섞지 마세요. | # 디렉터리 전체를 한 단위로 복사합니다. 서로 다른 시각의 파일을 섞지 마세요. | ||
# 아래의 격리 복원 시험을 정기적으로 실행합니다. checksum은 전송 무결성만 확인하며 실제 복원 가능성을 보장하지 않습니다. | # 아래의 격리 복원 시험을 정기적으로 실행합니다. checksum은 전송 무결성만 확인하며 실제 복원 가능성을 보장하지 않습니다. | ||
MediaWiki [https://www.mediawiki.org/wiki/Manual:Backing_up_a_wiki 백업 안내]도 DB와 파일을 함께 보관하도록 안내합니다. XML 페이지 내보내기는 보조 이동본으로 쓸 수 있지만 사용자 계정, 모든 로그, 업로드 메타데이터, 사이트 전체 상태를 담지 않으므로 이 복구 세트를 대신할 수 없습니다. | MediaWiki [https://www.mediawiki.org/wiki/Manual:Backing_up_a_wiki 백업 안내]도 DB와 파일을 함께 보관하도록 안내합니다. XML 페이지 내보내기는 보조 이동본으로 쓸 수 있지만 사용자 계정, 모든 로그, 업로드 메타데이터, 사이트 전체 상태를 담지 않으므로 이 복구 세트를 대신할 수 없습니다. | ||
==== MariaDB PITR 경계 ==== | |||
<code><nowiki>database.sql.gz</nowiki></code>는 완전 기준 백업입니다. <code><nowiki>pitr-manifest.txt</nowiki></code>의 <code><nowiki>binlog_file</nowiki></code>·<code><nowiki>binlog_position</nowiki></code>은 그 덤프가 이미 포함한 마지막 경계이자 replay 시작 좌표입니다. 이 좌표 이전 event를 다시 적용하면 중복 변경이 생깁니다. 이후 시각으로 이동하려면 기준 좌표 '''이후'''의 연속된 binary log가 전부 있어야 합니다. 백업 당시 archive에는 아직 생기지 않은 미래 event가 들어 있을 수 없으므로, 더 늦게 만든 복구 세트의 <code><nowiki>database-binlogs.tar.gz</nowiki></code>나 별도 연속 보관본이 필요합니다. 로컬 <code><nowiki>db_binlogs</nowiki></code>는 기본 14일 후 자동 만료되므로 원격 백업과 복원 시험이 이 기간 안에 성공해야 합니다. | |||
PITR은 운영 DB에 바로 재생하지 마세요. 격리 복원을 먼저 완료한 뒤, <code><nowiki>mariadb-binlog --start-position</nowiki></code>에는 manifest의 좌표 다음 event부터 적용되도록 경계를 확인하고, <code><nowiki>--stop-datetime</nowiki></code> 또는 <code><nowiki>--stop-position</nowiki></code>을 명시하여 SQL을 만듭니다. 재생 전에 시작·종료 file, position, target UTC, 예상 event 수를 작업 기록에 남기고, Lesson·revision 개수와 대표 페이지를 확인합니다. binary log가 하나라도 빠졌거나 기준 position보다 이전 백업과 섞였다면 PITR를 계속하지 말고 다음 완전 백업으로 복원합니다. | |||
==== 권장 주기 ==== | ==== 권장 주기 ==== | ||
| 93번째 줄: | 145번째 줄: | ||
* 분기마다 격리 복원 시험 | * 분기마다 격리 복원 시험 | ||
실제 복구 시점 목표는 운영 상황에 맞게 정합니다. 스케줄러 종료 상태와 디스크 여유 공간을 확인하세요. 오류 알림이 없는 예약 명령만으로는 충분하지 않습니다. 새 복원을 확인하기 전에 가장 최근의 정상 백업을 지우지 마세요. | 실제 복구 시점 목표는 운영 상황에 맞게 정합니다. 스케줄러 종료 상태와 디스크 여유 공간을 확인하세요. 오류 알림이 없는 예약 명령만으로는 충분하지 않습니다. 새 복원을 확인하기 전에 가장 최근의 정상 백업을 지우지 마세요. | ||
==== Cloudflare R2 자동 백업 ==== | |||
이 서버는 다음 비공개 R2 bucket의 <code><nowiki>restic</nowiki></code> prefix를 원격 백업 저장소로 사용합니다. | |||
<pre><nowiki> | |||
https://96f01030a087d07d906bff299d260155.r2.cloudflarestorage.com/s3rm-backups | |||
</nowiki></pre> | |||
백업 데이터는 <code><nowiki>restic</nowiki></code>이 서버에서 먼저 암호화한 뒤 전송합니다. R2 Access Key, Secret Key와 restic 암호는 Git이나 프로젝트 <code><nowiki>.env</nowiki></code>에 넣지 않습니다. | |||
서버에 <code><nowiki>restic</nowiki></code>을 설치한 뒤 root 권한으로 설정 파일을 준비합니다. | |||
<pre><nowiki> | |||
sudo install -d -m 0700 /etc/s3-research-memory /srv/s3rm-backups | |||
sudo install -m 0600 config/remote-backup.env.example \ | |||
/etc/s3-research-memory/remote-backup.env | |||
sudo sh -c 'umask 077; openssl rand -base64 48 > /etc/s3-research-memory/restic-password' | |||
sudoedit /etc/s3-research-memory/remote-backup.env | |||
</nowiki></pre> | |||
R2에서 <code><nowiki>s3rm-backups</nowiki></code> bucket에만 목록 조회·읽기·쓰기·삭제가 가능한 S3 API 자격 증명을 만들고 설정 파일의 두 placeholder를 바꿉니다. 삭제 권한은 오래된 restic snapshot과 pack을 보존 정책에 맞게 정리할 때 필요합니다. restic 암호 파일은 서버 장애 때도 찾을 수 있도록 서버 밖의 비밀번호 관리자에 따로 보관합니다. 이 암호를 잃으면 R2의 백업을 복구할 수 없습니다. | |||
현재 live restic repository의 자격 증명은 <code><nowiki>forget</nowiki></code>·<code><nowiki>prune</nowiki></code>를 위해 삭제 권한도 가집니다. 따라서 '''R2 자격 증명 탈취와 오작동으로부터의 불변 백업은 현재 저장소 하나만으로 달성되지 않습니다.''' 이 제한은 복원 시험 기록에 항상 남깁니다. | |||
R2의 lock·retention을 live restic prefix에 바로 적용하면 임시 lock 제거와 <code><nowiki>forget</nowiki></code>·<code><nowiki>prune</nowiki></code>가 실패할 수 있습니다. 삭제 방어는 다음과 같이 '''별도 보호 복사본'''으로 구성합니다. | |||
# live <code><nowiki>restic</nowiki></code> prefix는 현재 보존 정책으로 검증·정리합니다. | |||
# 검증한 복구 세트 또는 restic repository 세대를 별도 bucket·account의 보호 prefix로 복사합니다. 복사 writer와 retention 변경 권한을 분리합니다. | |||
# 보호 복사본은 일간 운영 키로 삭제할 수 없게 만들고, 예외 해제 권한은 다른 계정에 보관합니다. | |||
# Cloudflare 계정 MFA, bucket 범위 API token, 삭제·retention 변경 알림, 분기별 보호 복사본 복원 시험을 함께 적용합니다. | |||
별도 bucket/retention/API token은 Cloudflare 외부 설정이므로 이 저장소의 Compose나 systemd 파일만으로는 활성화할 수 없습니다. 이 설정이 완료되기 전에는 live R2를 유일한 삭제 방어 본으로 간주하지 마세요. | |||
원격 저장소는 한 번만 초기화합니다. | |||
<pre><nowiki> | |||
sudo bash -c ' | |||
set -a | |||
source /etc/s3-research-memory/remote-backup.env | |||
set +a | |||
exec /home/mrcha033/Projects/s3wiki/scripts/init-remote-backup.sh | |||
' | |||
</nowiki></pre> | |||
systemd 단위를 설치하고 첫 백업을 수동으로 확인합니다. | |||
<pre><nowiki> | |||
sudo install -m 0644 deploy/systemd/s3rm-remote-backup.service \ | |||
/etc/systemd/system/s3rm-remote-backup.service | |||
sudo install -m 0644 deploy/systemd/s3rm-remote-backup.timer \ | |||
/etc/systemd/system/s3rm-remote-backup.timer | |||
sudo systemctl daemon-reload | |||
sudo systemctl start s3rm-remote-backup.service | |||
sudo systemctl status s3rm-remote-backup.service | |||
sudo systemctl enable --now s3rm-remote-backup.timer | |||
systemctl list-timers s3rm-remote-backup.timer | |||
</nowiki></pre> | |||
작업은 매일 서울 시각 03:15 이후 45분 안에 실행합니다. 성공한 원격 snapshot은 일간 14개와 주간 8개를 보존하고, 원격 pack 정리는 일요일에 실행합니다. 전송이나 검증이 실패하면 로컬 복구 세트를 지우지 않습니다. 성공하면 R2에 이미 암호화된 snapshot을 확인한 뒤 <code><nowiki>/srv/s3rm-backups</nowiki></code>의 해당 임시 세트를 정리합니다. snapshot 경로에는 매번 다른 시각이 들어가므로 restic 보존 그룹은 <code><nowiki>host,tags</nowiki></code>로만 묶습니다. 경로까지 그룹 기준에 넣으면 각 snapshot이 항상 자신만의 그룹이 되어 14일·8주 정책이 아무 것도 만료시키지 못합니다. | |||
상태와 최근 로그를 정기적으로 확인합니다. | |||
<pre><nowiki> | |||
systemctl status s3rm-remote-backup.timer | |||
sudo journalctl -u s3rm-remote-backup.service --since '2 days ago' | |||
</nowiki></pre> | |||
원격 snapshot을 복구할 때는 빈 root 전용 디렉터리에 먼저 내려받고 기존 복원 스크립트로 넘깁니다. | |||
<pre><nowiki> | |||
sudo install -d -m 0700 /srv/s3rm-remote-restore | |||
sudo bash -c ' | |||
set -a | |||
source /etc/s3-research-memory/remote-backup.env | |||
set +a | |||
restic snapshots --tag s3rm-recovery-set | |||
restic restore latest --tag s3rm-recovery-set --target /srv/s3rm-remote-restore | |||
' | |||
sudo find /srv/s3rm-remote-restore -maxdepth 2 -type f -name SHA256SUMS -print | |||
</nowiki></pre> | |||
출력된 <code><nowiki>SHA256SUMS</nowiki></code>의 상위 디렉터리가 한 개이고 원하는 시각의 복구 세트인지 확인한 뒤 <code><nowiki>scripts/restore.sh <그 디렉터리> --yes</nowiki></code>를 사용합니다. 운영 서버를 덮어쓰기 전에 격리 복원 시험을 먼저 수행합니다. | |||
=== Lesson 내용 정기 감사 === | |||
읽기 전용 감사 작업은 매주 운영 위키의 안정된 Lesson을 중앙 <code><nowiki>mcp-http</nowiki></code>에서 읽습니다. 실행 전 30분 안에 바뀐 Lesson은 다음 감사로 미룹니다. 별도 Lesson 저장소나 기준 파일을 만들지 않습니다. 쓰기 토큰도 사용하지 않습니다. 외부 근거 URL을 열거나 Lesson을 자동으로 수정하지 않습니다. | |||
감사 작업은 다음 항목을 확인합니다. | |||
* 구조화된 근거가 없는 Lesson | |||
* <code><nowiki>high</nowiki></code> confidence인데 최신 append-only 검증 기록이 observation, interpretation, reusable_lesson 전부를 strong <code><nowiki>supports</nowiki></code>로 뒷받침하지 않는 Lesson | |||
* 확인 범위가 <code><nowiki>not_recorded</nowiki></code>인 근거 | |||
* 같거나 거꾸로 된 <code><nowiki>verified_at</nowiki></code>으로 append 순서의 현재 판정을 정할 수 없는 Lesson | |||
* 서로 같은 관찰·해석 또는 재사용 내용·적용 조건 | |||
* 대상이 없는 relation과 중복 relation | |||
* 365일 이상 바뀌지 않은 검토 후보 | |||
* 비밀 키나 토큰 형식으로 보이는 문자열 | |||
발견 결과에는 Lesson ID, revision ID, <code><nowiki>codes</nowiki></code> 배열과 상한이 있는 evidence·verification ID만 남깁니다. Lesson 본문, 인용과 relation note는 journal에 남기지 않습니다. 같은 Lesson의 발견 코드는 한 줄로 묶습니다. 오래된 Lesson이라는 이유만으로 틀렸다고 판정하지 않으며 <code><nowiki>stale_review_candidate</nowiki></code>로만 표시합니다. 감사는 저장된 구조, digest, append 순서와 검증 수령 기록의 일관성을 확인합니다. 외부 근거 URL을 열지 않으므로 논문·코드·데이터의 사실 진위를 자동으로 판정하지 않습니다. | |||
systemd 단위를 설치하고 첫 감사를 수동으로 실행합니다. | |||
<pre><nowiki> | |||
sudo install -D -m 0755 scripts/audit-lessons.py \ | |||
/usr/local/libexec/s3rm-content-audit | |||
sudo install -m 0644 deploy/systemd/s3rm-content-audit.service \ | |||
/etc/systemd/system/s3rm-content-audit.service | |||
sudo install -m 0644 deploy/systemd/s3rm-content-audit.timer \ | |||
/etc/systemd/system/s3rm-content-audit.timer | |||
sudo systemctl daemon-reload | |||
sudo systemctl start s3rm-content-audit.service | |||
sudo systemctl status s3rm-content-audit.service | |||
sudo journalctl -u s3rm-content-audit.service --since today | |||
sudo systemctl enable --now s3rm-content-audit.timer | |||
systemctl list-timers s3rm-content-audit.timer | |||
</nowiki></pre> | |||
작업은 일요일 서울 시각 04:30 이후 30분 안에 실행합니다. 놓친 실행은 서버가 다시 켜졌을 때 실행합니다. 배포 단위와 <code><nowiki>make audit-lessons</nowiki></code>는 현재 서버의 scan limit과 같은 500개 Lesson을 읽습니다. 스크립트 자체의 기본값은 200개이고 설정 가능한 절대 상한은 1000개입니다. Lesson당 근거·relation은 각각 최대 200개를 읽습니다. 서버의 <code><nowiki>scan_limit</nowiki></code>에서 목록이 잘리거나 감사 중 revision과 Lesson 목록이 바뀌면 완료로 기록하지 않습니다. 연결 오류나 동시 변경으로 끝나면 5분 간격으로 최대 세 번 시도합니다. 발견 사항을 뜻하는 종료 코드 <code><nowiki>2</nowiki></code>는 다시 시도하지 않습니다. | |||
종료 코드는 다음과 같습니다. | |||
{| class="wikitable" | |||
! 코드 !! 뜻 !! 다음 작업 | |||
|- | |||
| <code><nowiki>0</nowiki></code> || 감사를 완료했고 발견 사항이 없음 || 다음 예약 실행을 기다립니다. | |||
|- | |||
| <code><nowiki>1</nowiki></code> || 연결, 출력 한도 또는 동시 변경으로 감사를 완료하지 못함 || journal의 마지막 오류를 확인하고 다시 실행합니다. | |||
|- | |||
| <code><nowiki>2</nowiki></code> || 감사를 완료했고 확인할 항목이 있음 || 발견된 Lesson을 읽고 근거를 확인합니다. | |||
|} | |||
<code><nowiki>systemd</nowiki></code> 서비스는 임시 계정으로 실행합니다. 홈 디렉터리, <code><nowiki>.env</nowiki></code>, Docker 소켓과 <code><nowiki>/etc/s3-research-memory</nowiki></code>를 읽을 수 없습니다. 발견 사항이 있으면 실패 상태로 표시합니다. 이 상태는 Lesson을 바꾸지 않으며 다음 예약 실행도 막지 않습니다. 문제를 고칠 때는 근거를 먼저 붙인 뒤 같은 Lesson ID에 현재 revision을 기준으로 새 revision을 추가합니다. <code><nowiki>supersedes</nowiki></code>나 근거 note를 기존 주장 수정 수단으로 쓰지 않습니다. | |||
예약과 무관하게 현재 상태를 확인할 때는 다음 명령을 사용합니다. 발견 사항이 있어도 수동 명령은 감사 완료 뒤 코드 <code><nowiki>0</nowiki></code>을 반환합니다. 자동 점검과 같은 종료 코드가 필요하면 <code><nowiki>--fail-on-findings</nowiki></code>를 추가합니다. | |||
<pre><nowiki> | |||
make audit-lessons | |||
</nowiki></pre> | |||
=== 복원 전 주의 === | === 복원 전 주의 === | ||
| 111번째 줄: | 300번째 줄: | ||
<pre><nowiki> | <pre><nowiki> | ||
docker compose stop mcp-http | docker compose --profile domain stop caddy mcp-http | ||
./scripts/restore.sh /ABS/PATH/TO/BACKUP-DIRECTORY --yes | ./scripts/restore.sh /ABS/PATH/TO/BACKUP-DIRECTORY --yes | ||
</nowiki></pre> | </nowiki></pre> | ||
스크립트는 checksum manifest, 백업 형식 버전, 압축 stream, 모든 아카이브 member의 경로와 종류를 먼저 확인합니다. 백업의 DB 이름과 <code><nowiki>MARIADB_DATABASE</nowiki></code>도 같아야 합니다. 다르면 덤프를 편집하지 말고 복구용 <code><nowiki>.env</nowiki></code>를 맞춥니다. 이 검증과 이미지 빌드 사전 확인이 모두 끝난 뒤에만 애플리케이션을 멈추고 | 스크립트는 checksum manifest, 백업 형식 버전, 압축 stream, 모든 아카이브 member의 경로와 종류를 먼저 확인합니다. 보조 파일이 있는 형식은 auxiliary manifest digest와 binary-log·source artifact도 모두 확인합니다. 백업의 DB 이름과 <code><nowiki>MARIADB_DATABASE</nowiki></code>도 같아야 합니다. 다르면 덤프를 편집하지 말고 복구용 <code><nowiki>.env</nowiki></code>를 맞춥니다. 이 검증과 이미지 빌드 사전 확인이 모두 끝난 뒤에만 애플리케이션을 멈추고 기준 DB·설정·업로드를 교체합니다. | ||
기준 dump import는 <code><nowiki>sql_log_bin=0</nowiki></code> 세션에서 실행합니다. 이어서 DB를 멈추고 기존 <code><nowiki>db_binlogs</nowiki></code>의 '''더 새로운 운영 timeline을 비운 뒤''' 새 local timeline으로 시작합니다. 따라서 과거 DB에 미래 binlog가 이어지는 일은 없지만, backup의 archived binlog를 자동 replay하지도 않습니다. PITR이 필요하면 운영 복원 전에 별도 격리 환경에서 목표 UTC·종료 position을 정해 검증한 절차로만 적용합니다. | |||
<code><nowiki>mediawiki</nowiki></code> 시작 때 MediaWiki·확장 기능 마이그레이션을 적용하고, 이어서 반복 실행이 가능한 <code><nowiki>bootstrap</nowiki></code>을 | <code><nowiki>mediawiki</nowiki></code> 시작 때 MediaWiki·확장 기능 마이그레이션을 적용하고, 이어서 반복 실행이 가능한 <code><nowiki>bootstrap</nowiki></code>을 적용합니다. 그 뒤 <code><nowiki>mediawiki-job-state-init</nowiki></code>을 강제로 다시 실행해 새 복구 볼륨을 UID/GID 33 쓰기 경로로 만든 다음 bounded <code><nowiki>mediawiki-jobs</nowiki></code>를 시작해 worker health까지 확인합니다. 복구 중 MediaWiki host bind는 임시로 loopback에 강제하고 <code><nowiki>caddy</nowiki></code>와 <code><nowiki>mcp-http</nowiki></code>를 성공 후에도 중지한 상태로 둡니다. 따라서 마이그레이션· bootstrap 중 공개 브라우저나 MCP 쓰기가 재개되지 않습니다. 교체 경고가 나온 뒤에는 중단하지 마세요. 오류가 나오면 클라이언트를 멈춘 상태로 로그와 소스 백업을 보존합니다. 실패한 상태 위에 부분 SQL이나 일부 볼륨을 임의로 덮어쓰지 마세요. | ||
완료 뒤 상태를 확인합니다. | 완료 뒤 상태를 확인합니다. | ||
| 123번째 줄: | 314번째 줄: | ||
<pre><nowiki> | <pre><nowiki> | ||
docker compose ps -a | docker compose ps -a | ||
docker compose logs mediawiki bootstrap | docker compose logs mediawiki bootstrap mediawiki-jobs | ||
</nowiki></pre> | </nowiki></pre> | ||
| 129번째 줄: | 320번째 줄: | ||
<pre><nowiki> | <pre><nowiki> | ||
docker compose up -d --force-recreate mcp-http | docker compose --profile domain up -d --force-recreate mediawiki mcp-http caddy | ||
docker compose ps mcp-http | docker compose ps mediawiki-jobs mcp-http | ||
docker compose logs --tail=100 mcp-http | docker compose logs --tail=100 mcp-http | ||
./scripts/verify-public-domain.sh s3wiki.yonsei.ac.kr | |||
</nowiki></pre> | </nowiki></pre> | ||
| 137번째 줄: | 329번째 줄: | ||
실제 서버를 덮어쓰지 말고 별도 Compose 프로젝트와 사용하지 않는 포트에서 시험합니다. 별도 checkout을 사용하고, 복구 디렉터리는 이동하지 말고 복사합니다. Compose 프로젝트와 볼륨 이름을 고유하게 지정하세요. 최상위 이름은 <code><nowiki>COMPOSE_PROJECT_NAME</nowiki></code>으로 바꿀 수 있으며 두 스크립트가 이 값을 사용합니다. <code><nowiki>COMPOSE</nowiki></code> 환경 변수에는 <code><nowiki>docker compose --ansi never</nowiki></code>처럼 고정 접두사를 넣을 수 있습니다. 설치, 백업, 복원에 같은 값을 사용하세요. | 실제 서버를 덮어쓰지 말고 별도 Compose 프로젝트와 사용하지 않는 포트에서 시험합니다. 별도 checkout을 사용하고, 복구 디렉터리는 이동하지 말고 복사합니다. Compose 프로젝트와 볼륨 이름을 고유하게 지정하세요. 최상위 이름은 <code><nowiki>COMPOSE_PROJECT_NAME</nowiki></code>으로 바꿀 수 있으며 두 스크립트가 이 값을 사용합니다. <code><nowiki>COMPOSE</nowiki></code> 환경 변수에는 <code><nowiki>docker compose --ansi never</nowiki></code>처럼 고정 접두사를 넣을 수 있습니다. 설치, 백업, 복원에 같은 값을 사용하세요. | ||
권장 경로는 전용 검증 스크립트입니다. 운영 <code><nowiki>.env</nowiki></code>가 아닌 mode <code><nowiki>0600</nowiki></code> 일회용 env에 사용하지 않는 loopback 포트, 일회용 DB·wiki 비밀번호, <code><nowiki>MW_SERVER_URL=http://127.0.0.1:18080</nowiki></code>과 <code><nowiki>S3RM_WIKI_BIND_ADDRESS=127.0.0.1</nowiki></code>, <code><nowiki>S3RM_HTTP_PORT=18080</nowiki></code>을 넣습니다. 복원용 <code><nowiki>S3RM_WIKI_BOT_PASSWORD</nowiki></code>도 <code><nowiki>openssl rand -hex 16</nowiki></code>으로 따로 만들고, 운영 BotPassword와 primary 위키 비밀번호를 재사용하지 않습니다. 검증 스크립트는 DB를 가져온 직후 격리 project에서만 복원된 관리자 비밀번호를 일회용 env의 관리자 secret으로 바꿉니다. secret은 명령행 인자나 로그에 넣지 않으며, 격리 환경을 지울 때 변경된 관리자 해시도 함께 없어집니다. 운영 project에는 이 비밀번호 재설정 옵션을 사용할 수 없습니다. 실제 Compose project 이름과 모든 volume·network 이름도 함께 검사하므로 <code><nowiki>COMPOSE</nowiki></code> 접두사는 검증 스크립트가 만든 고정 명령만 허용합니다. 따라서 다른 <code><nowiki>--project-name</nowiki></code>, Compose override, bind mount, 외부 volume 또는 별칭을 덧붙여 이 경계를 우회할 수 없습니다. bootstrap의 관리자·MCP 기본 계정·BotPassword도 secret 파일에서 직접 읽으며 평문을 자식 프로세스 환경이나 명령행 인자로 전달하지 않습니다. | |||
<pre><nowiki> | |||
chmod 0600 /srv/s3rm-restore-test.env | |||
./scripts/verify-restore-isolated.sh /ABS/PATH/TO/BACKUP-DIRECTORY \ | |||
--env-file /srv/s3rm-restore-test.env \ | |||
--project-name s3rm-restore-2026q3 | |||
</nowiki></pre> | |||
스크립트는 project 접두사, 새 project에 기존 컨테이너·볼륨·네트워크가 없는지와 resolved Compose가 사용할 실제 volume·network 이름도 모두 비어 있는지, loopback 주소, 기본 checksum과, 있는 경우 PITR 보조 checksum 체인을 '''변경 전''' 확인합니다. 격리 project 이름으로 전용 MediaWiki image tag를 만들어 운영 image tag를 동시에 다시 빌드하지 않습니다. 복원 후 Lesson 개수는 정확히 일치해야 하고, revision은 bootstrap이 관리 페이지 변경을 추가할 수 있으므로 백업 기록 '''이상'''이어야 합니다. MediaWiki API, bootstrap, bounded <code><nowiki>mediawiki-jobs</nowiki></code> health도 확인합니다. 성공하면 격리 볼륨을 지우고, 실패하면 진단을 위해 보존합니다. 성공 시 project 전용 MediaWiki image tag도 함께 지웁니다. 자동 정리가 실패하면 스크립트가 실패 상태와 정확한 수동 정리 명령을 출력합니다. <code><nowiki>--keep</nowiki></code>을 사용하면 성공한 환경도 보존합니다. 이 검증은 스키마·개수·시작 경계를 자동 확인하지만, 아래의 사람이 열어 보는 대표 Lesson·업로드·검색 확인을 대신하지 않습니다. | |||
Docker가 설치된 일회용 VM에서는 다음 순서가 안전합니다. | Docker가 설치된 일회용 VM에서는 다음 순서가 안전합니다. | ||
| 151번째 줄: | 354번째 줄: | ||
다음 항목을 모두 통과해야 복원이 끝납니다. | 다음 항목을 모두 통과해야 복원이 끝납니다. | ||
# <code><nowiki>db</nowiki></code> | # <code><nowiki>db</nowiki></code>, <code><nowiki>mediawiki</nowiki></code>, <code><nowiki>mediawiki-jobs</nowiki></code>가 healthy이고 <code><nowiki>mediawiki-install</nowiki></code>과 <code><nowiki>bootstrap</nowiki></code>이 코드 0으로 종료되었습니다. | ||
# <code><nowiki>Special:Version</nowiki></code>에 예상한 MediaWiki, Semantic MediaWiki, Page Forms, S3 Research Memory 버전이 표시됩니다. | # <code><nowiki>Special:Version</nowiki></code>에 예상한 MediaWiki, Semantic MediaWiki, Page Forms, S3 Research Memory 버전이 표시됩니다. | ||
# Lesson 페이지 수와 최근 revision 수가 백업 기록과 맞습니다. | # Lesson 페이지 수와 최근 revision 수가 백업 기록과 맞습니다. | ||
# 복원된 Lesson의 필드, 근거, 관계, 출처, 호환 필드, 시간이 정상 표시됩니다. | # 복원된 Lesson의 필드, 근거, append-only 검증 기록, 관계, 출처, 호환 필드, 시간이 정상 표시됩니다. | ||
# '''역사 보기'''에서 이전 revision 하나 이상이 열립니다. | # '''역사 보기'''에서 이전 revision 하나 이상이 열립니다. | ||
# 업로드한 근거가 열리고, 외부 checksum을 기록했다면 값이 맞습니다. | # 업로드한 근거가 열리고, 외부 checksum을 기록했다면 값이 맞습니다. | ||
| 170번째 줄: | 373번째 줄: | ||
<pre><nowiki> | <pre><nowiki> | ||
docker compose ps -a | docker compose ps -a | ||
docker compose logs --tail=200 db mediawiki bootstrap mcp-http caddy | docker compose logs --tail=200 db mediawiki-install mediawiki bootstrap \ | ||
mediawiki-jobs mcp-http caddy | |||
</nowiki></pre> | </nowiki></pre> | ||
| 181번째 줄: | 385번째 줄: | ||
curl --fail --silent --show-error \ | curl --fail --silent --show-error \ | ||
'http://127.0.0.1:8765/healthz' \ | 'http://127.0.0.1:8765/healthz' \ | ||
| jq --exit-status \ | |||
'.canonical_store == "ok" and | |||
.job_runner == "ok" and | |||
.write_admission.active >= 0 and .write_admission.waiting >= 0' \ | |||
>/dev/null | >/dev/null | ||
curl --fail --silent --show-error \ | curl --fail --silent --show-error \ | ||
| 187번째 줄: | 395번째 줄: | ||
</nowiki></pre> | </nowiki></pre> | ||
[[Help:S3 연구 메모리/설치와 검증#클라이언트를 연결하기 전 공용 MCP 확인| | [[Help:S3 연구 메모리/설치와 검증#클라이언트를 연결하기 전 공용 MCP 확인|<code><nowiki>setup.md</nowiki></code>]]의 토큰 없는 MCP 초기화도 <code><nowiki>https://s3wiki.yonsei.ac.kr/mcp</nowiki></code>에서 실행합니다. 단순 <code><nowiki>GET /mcp</nowiki></code>는 streaming 연결을 계속 열어 둘 수 있으므로 상태 확인이 아닙니다. | ||
같은 서버에서 위 확인을 한 번에 실행하려면 <code><nowiki>make verify-domain</nowiki></code>을 사용합니다. 이 결과만으로 학교 밖 인바운드 경로까지 확인되지는 않습니다. | 같은 서버에서 위 확인을 한 번에 실행하려면 <code><nowiki>make verify-domain</nowiki></code>을 사용합니다. 이 결과만으로 학교 밖 인바운드 경로까지 확인되지는 않습니다. | ||
MediaWiki | Compose 운영의 <code><nowiki>/healthz</nowiki></code>는 canonical store와 bounded job runner가 모두 준비돼야 200을 반환합니다. <code><nowiki>job_runner=unavailable</nowiki></code>이면 공유 상태 파일의 generation·최근 성공 시각·권한을 먼저 확인합니다. <code><nowiki>write_admission</nowiki></code>에는 비밀값 없이 현재 active·waiting·principal 수와 동시 실행·queue 상한이 나옵니다. 기본은 전체 동시 쓰기 8개, queue 64개·2초, principal별 지속 600회/분·burst 120입니다. <code><nowiki>open</nowiki></code> 모드에서는 client IP가 principal이므로 인증 모드를 바꾸지 않아도 사용자별 admission이 적용됩니다. 지표가 계속 상한에 붙거나 <code><nowiki>server_busy</nowiki></code>가 반복되면 DB latency와 실패 요청을 먼저 확인하고, 변경한 상한은 <code><nowiki>mcp-http</nowiki></code>를 다시 만들어 적용합니다. 이 queue는 재시작 뒤 작업을 복구하는 durable queue가 아닙니다. 전달된 IP는 Caddy와 MCP의 <code><nowiki>S3RM_MCP_PROXY_SECRET</nowiki></code>이 맞을 때만 신뢰합니다. 이 값을 교체할 때는 둘을 같은 Compose 재생성에서 함께 전환합니다. | ||
MediaWiki job queue는 전용 <code><nowiki>mediawiki-jobs</nowiki></code> 서비스가 기본 25건·20초, 1 process로 처리합니다. 상태 파일은 비밀 값이 없고 현재 queue 깊이, 마지막 성공 시각, 연속 실패 수, batch/idle 상한과 현재 runner generation을 담습니다. generation은 컨테이너 PID 1의 시작 시각에서 만들므로 이전 컨테이너나 복구 전 상태 파일은 새 runner health로 인정되지 않습니다. 상태 볼륨은 <code><nowiki>mediawiki-job-state-init</nowiki></code>이 UID/GID 33으로 준비하고 MCP에는 읽기 전용으로만 연결합니다. | |||
<pre><nowiki> | <pre><nowiki> | ||
docker compose ps mediawiki-jobs | |||
docker compose ps -a mediawiki-job-state-init | |||
docker compose exec mediawiki-jobs cat /run/s3rm-jobs/status.env | |||
docker compose exec mediawiki-jobs /usr/local/bin/s3-mediawiki-jobs-health | |||
docker compose exec mediawiki \ | docker compose exec mediawiki \ | ||
php maintenance/run.php | php maintenance/run.php showJobs --group | ||
</nowiki></pre> | </nowiki></pre> | ||
runner가 실패하면 15초에서 시작해 최대 5분까지 지수 backoff합니다. 마지막 성공이 기본 120초보다 오래되면 health가 실패합니다. queue를 빠르게 비우기 위해 <code><nowiki>--nothrottle</nowiki></code>이나 다중 process를 임의로 사용하지 마세요. 먼저 실패 job 종류와 DB·디스크 상태를 확인하고, 필요한 처리량만 <code><nowiki>.env</nowiki></code>에서 조정합니다. | |||
Compose는 각 주요 서비스의 json-file 로그를 파일당 10MB, 최대 5개로 회전하고, PID·메모리·CPU 상한을 둡니다. 평소 사용량과 OOM·재시작 여부를 함께 확인합니다. | |||
<pre><nowiki> | |||
docker stats --no-stream | |||
docker inspect --format \ | |||
'{{.Name}} oom={{.State.OOMKilled}} restarts={{.RestartCount}}' \ | |||
$(docker compose ps -q) | |||
docker system df | |||
</nowiki></pre> | |||
상한 도달은 health check 성공만으로 보이지 않을 수 있습니다. OOM이나 PID 부족을 확인한 뒤 queue·동시성·실패 loop 원인을 먼저 고치고, 측정한 정상 peak와 호스트 여유를 근거로 상한을 조정합니다. 로그 회전 파일은 장기 감사 자료가 아니므로 필요한 운영 event는 별도 제한된 저장소에 보관합니다. | |||
<code><nowiki>Special:RecentChanges</nowiki></code>, 계정 그룹, 로그인 실패, MCP 쓰기 인증 실패와 403/421, 디스크 여유 공간, 백업 시각, 복원 시험 상태를 확인합니다. <code><nowiki>bootstrap</nowiki></code>이 관리하는 스키마·양식 페이지가 반복해서 달라지면 원인을 찾으세요. 일반 Lesson revision은 <code><nowiki>bootstrap</nowiki></code>이 바꾸지 않습니다. | <code><nowiki>Special:RecentChanges</nowiki></code>, 계정 그룹, 로그인 실패, MCP 쓰기 인증 실패와 403/421, 디스크 여유 공간, 백업 시각, 복원 시험 상태를 확인합니다. <code><nowiki>bootstrap</nowiki></code>이 관리하는 스키마·양식 페이지가 반복해서 달라지면 원인을 찾으세요. 일반 Lesson revision은 <code><nowiki>bootstrap</nowiki></code>이 바꾸지 않습니다. | ||
| 232번째 줄: | 460번째 줄: | ||
# 원문을 출력하지 않고 새 토큰과 해시를 만듭니다. ```bash ./scripts/configure-mcp-token.sh --rotate ``` 스크립트는 <code><nowiki>.env</nowiki></code>의 해시만 바꾸고, 원문 토큰과 URL은 mode <code><nowiki>0600</nowiki></code> <code><nowiki>~/.config/s3-research-memory/client.env</nowiki></code>에 씁니다. <code><nowiki>--rotate</nowiki></code>가 없으면 기존 토큰과 해시가 맞는지 확인하고 그대로 둡니다. | # 원문을 출력하지 않고 새 토큰과 해시를 만듭니다. ```bash ./scripts/configure-mcp-token.sh --rotate ``` 스크립트는 <code><nowiki>.env</nowiki></code>의 해시만 바꾸고, 원문 토큰과 URL은 mode <code><nowiki>0600</nowiki></code> <code><nowiki>~/.config/s3-research-memory/client.env</nowiki></code>에 씁니다. <code><nowiki>--rotate</nowiki></code>가 없으면 기존 토큰과 해시가 맞는지 확인하고 그대로 둡니다. | ||
# 공용 어댑터만 다시 만듭니다. ```bash docker compose up -d --force-recreate mcp-http docker compose ps mcp-http docker compose logs --tail=100 mcp-http ``` | # 공용 어댑터만 다시 만듭니다. ```bash docker compose up -d --force-recreate mcp-http docker compose ps mcp-http docker compose logs --tail=100 mcp-http ``` | ||
# 토큰 없이 초기화, 도구 목록, <code><nowiki>search_lessons(limit=1)</nowiki></code>을 확인합니다. 이어서 신뢰하는 셸에서 새 비공개 파일을 불러오고 [[Help:S3 연구 메모리/설치와 검증| | # 토큰 없이 초기화, 도구 목록, <code><nowiki>search_lessons(limit=1)</nowiki></code>을 확인합니다. 이어서 신뢰하는 셸에서 새 비공개 파일을 불러오고 [[Help:S3 연구 메모리/설치와 검증|<code><nowiki>setup.md</nowiki></code>]]의 쓰기 확인을 실행합니다. | ||
# 새 원문 토큰 또는 클라이언트 환경을 쓰기 클라이언트에 전달하고 재연결합니다. 공개 조회 클라이언트는 바꿀 것이 없습니다. DB·위키 비밀번호와 쓸 수 없는 토큰 해시가 든 서버 <code><nowiki>.env</nowiki></code>는 전달하지 마세요. | # 새 원문 토큰 또는 클라이언트 환경을 쓰기 클라이언트에 전달하고 재연결합니다. 공개 조회 클라이언트는 바꿀 것이 없습니다. DB·위키 비밀번호와 쓸 수 없는 토큰 해시가 든 서버 <code><nowiki>.env</nowiki></code>는 전달하지 마세요. | ||
새 서비스를 확인하기 전에 문제가 생겼다면 이전 해시와 그에 맞는 비공개 토큰을 복구하고 <code><nowiki>mcp-http</nowiki></code>를 다시 만듭니다. 변경 인증을 끄는 방식으로 되돌리지 마세요. | 새 서비스를 확인하기 전에 문제가 생겼다면 이전 해시와 그에 맞는 비공개 토큰을 복구하고 <code><nowiki>mcp-http</nowiki></code>를 다시 만듭니다. 변경 인증을 끄는 방식으로 되돌리지 마세요. | ||
2026년 7월 18일 (토) 20:01 기준 최신판
이 페이지는 저장소 문서에서 자동으로 동기화됩니다. 위키에서 직접 편집하지 마세요.
Semantic MediaWiki가 유일한 Lesson 저장소입니다. 복구 지점에는 같은 시점의 MariaDB 덤프, 업로드 파일, 생성된 위키 설정이 모두 있어야 합니다. 공용 mcp-http는 상태를 저장하지 않는 전송·검증 어댑터입니다. Lesson 볼륨, 토큰 DB, 별도 기준 데이터가 없으므로 백업하지 않습니다. 이미지와 설정은 저장소와 .env에서 다시 만듭니다.
Caddy는 [1]와 [2]의 HTTPS 진입점이며 지식을 저장하지 않습니다.
복구 스크립트는 Bash 4 이상, GNU coreutils(stat, sha256sum), GNU tar, gzip이 있는 GNU/Linux Docker 호스트에서 지원합니다. 필요한 명령이 없으면 바로 실패합니다. macOS와 Windows Docker Desktop에서는 이 Compose 프로젝트를 소유한 Linux VM 또는 Linux 호스트에서 실행하세요. BSD 명령 옵션이 같다고 가정하지 마세요.
영구 데이터
| Compose 볼륨 | 내용 | 복구 때 역할 |
|---|---|---|
db_data |
MediaWiki 페이지, 의미 데이터, 사용자, 변경 이력, 로그, 호환 필드 | 기준 데이터베이스 |
db_binlogs |
MariaDB ROW binary log와 index | 기준 덤프 이후 시간점 복구(PITR). 기본 14일 보존 |
mediawiki_images |
업로드한 근거 파일과 MediaWiki 업로드 메타데이터 | DB 덤프와 같은 시점이어야 함 |
mediawiki_config |
생성된 LocalSettings.php와 MediaWiki 비밀 키 |
위키 식별·설정 보존에 필요 |
caddy_data |
발급된 TLS 인증서, 개인 키, Caddy 실행 상태 | 평소 보존하되 Lesson 기준 데이터는 아님 |
caddy_config |
Caddy 실행 설정 | 저장소와 공개 hostname으로 다시 생성 가능 |
embedding_models |
SHA-256으로 확인한 번들 GGUF | 다시 받을 수 있는 파생 캐시. Lesson 복구 세트가 아님 |
일반 백업 스크립트는 DB 덤프, 설정, 업로드와 함께 db_binlogs의 캡처 시점 구간과 덤프에 내장된 replay 시작 file·position을 저장합니다. 이 항목이 Lesson 복구 세트입니다. 새 db_binlogs 볼륨은 one-shot db-binlog-init이 MariaDB 계정만 쓸 수 있도록 소유권과 mode를 맞춘 뒤 DB가 시작합니다. 이 서비스가 실패하면 DB를 시작하지 않으며, 볼륨 권한을 넓혀 우회하지 않습니다. caddy_data와 caddy_config는 평소 재시작 때 남겨 두되, 버전이 있는 위키 백업에 섞지 않습니다. 이 볼륨이 없어지면 DNS와 외부 80/443이 정상일 때 Caddy가 인증서를 다시 받을 수 있습니다. 일반 운영에서는 docker compose down --volumes를 쓰지 마세요.
embedding_models에는 Lesson, 검색 query나 벡터가 없습니다. 백업·복원 스크립트는 이 볼륨을 포함하지 않으며, 의미 검색을 다시 켤 때 고정된 원본과 SHA-256으로 재구성합니다. 평소에는 재다운로드와 서비스 중단을 피하도록 볼륨을 남겨 둡니다.
저장소에는 다시 만들 수 있는 코드, 템플릿, 양식이 있습니다. 자격 증명이 든 .env는 추적하지 않으며 일반 백업 아카이브에도 넣지 않습니다. 서버 담당자가 복구용 사본을 따로 보관하되 Git이나 공개 아카이브에는 넣지 마세요.
복구 세트의 runtime-images.txt는 Compose에 설정한 image reference와 실제 컨테이너 image ID를 함께 기록합니다. repository-tracked.patch.gz는 현재 HEAD 기준의 추적된 파일 차이만 담습니다. .env, untracked dump, upload, 자격 증명은 구조적으로 제외됩니다. untracked 파일이 있었다면 metadata에 present_not_archived만 남고 내용과 이름은 보관하지 않습니다. root systemd timer도 이 checkout의 정규화된 경로만 Git 명령별 safe.directory로 허용하므로 전역 Git 신뢰 설정을 바꾸지 않으면서 같은 source metadata와 patch를 만듭니다.
서버 .env에는 MCP 쓰기 토큰의 해시만 있습니다. 전달용 원문 토큰이 든 mode 0600 ~/.config/s3-research-memory/client.env도 최소 한 곳에 보관하세요. 원문 토큰을 모두 잃었다면 ./scripts/configure-mcp-token.sh --rotate로 새 토큰을 만들고 쓰기 클라이언트에 다시 전달합니다. 이 작업 중에도 공개 조회는 계속됩니다.
백업
저장소 밖의 절대 경로를 사용합니다. 가능하면 Docker 호스트와 다른 디스크를 선택하고, 서버 담당자만 읽고 쓸 수 있게 만듭니다.
install -d -m 0700 /srv/s3rm-backups docker compose stop mcp-http ./scripts/backup.sh /srv/s3rm-backups docker compose up -d mcp-http
먼저 mcp-http를 멈추면 에이전트 변경 경로가 닫힙니다. 백업 스크립트는 DB를 기다린 뒤 mediawiki-jobs, mediawiki와 실행 중인 bootstrap을 멈춰 브라우저와 job 변경도 막습니다. MariaDB binary log를 교체해 file·position을 고정한 뒤 --master-data=2 논리 덤프, binary-log 구간, 업로드·설정 볼륨을 묶습니다. 백업과 복구 스크립트는 같은 checkout 디렉터리에 nonblocking maintenance lock을 겁니다. 예약 백업, 수동 백업, 복구 중 둘 이상을 겹쳐 실행하면 나중 시작한 작업이 즉시 실패합니다. 다른 checkout으로 같은 운영 Compose project를 동시에 관리하지 마세요. 백업 종료 때는 mediawiki, 실행 중이던 bootstrap, mediawiki-jobs 순서로 복원하고 job worker health를 확인한 뒤에만 mcp-http를 다시 엽니다. worker가 준비되지 않으면 백업 파일 생성 여부와 관계없이 실패로 끝나며 MCP는 중지 상태를 유지합니다. 실패해도 exit trap이 스크립트가 관리한 서비스를 백업 전 상태로 돌립니다. mcp-http는 명시적으로 멈췄으므로 성공하거나 원인을 확인한 실패 뒤에도 마지막 up 명령을 실행해야 합니다.
모든 파일의 형식과 checksum을 확인한 뒤에만 시각이 붙은 최종 디렉터리를 공개합니다. 덤프나 근거 아카이브를 저장소에 넣지 않습니다. 끝에 출력되는 복구 디렉터리의 정확한 경로를 기록하세요.
binary log를 처음 켜는 서버
이 설정을 배포하기 전부터 실행 중인 DB는 컨테이너를 다시 만들기 전까지 log_bin=OFF일 수 있습니다. 이 상태에서 일반 백업은 중단되며, 최초 이전 전에만 다음 명시적 기준 백업을 사용합니다.
./scripts/backup.sh --legacy-base /srv/s3rm-backups
--legacy-base는 log_bin=OFF인 DB에서만 실행됩니다. database.sql.gz, 두 볼륨 아카이브, metadata.txt, SHA256SUMS로 이루어진 복원 가능한 format 1 기준 백업이며 PITR 보조 파일은 하나도 생성하지 않습니다. 나중에 별도 database-binlogs.tar.gz나 AUXILIARY-SHA256SUMS를 더해 일반 백업처럼 만들지 마세요. 보조 파일이 일부만 있는 세트는 복원과 격리 검증이 거부합니다.
체크섬을 확인하고 완성된 디렉터리를 원격 저장소에 복사한 뒤에만 DB를 재생성합니다. db_data는 지우지 않고 새 db_binlogs 볼륨을 연결합니다.
docker compose stop mediawiki-jobs mcp-http bootstrap mediawiki
docker compose up -d --force-recreate --wait db
docker compose exec -T db sh -eu -c '
MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb \
--host=127.0.0.1 --user=root --batch \
--execute="SHOW VARIABLES LIKE '\''log_bin'\''; SHOW MASTER STATUS;"
'
make domain-up
./scripts/backup.sh /srv/s3rm-backups
출력에 log_bin\tON과 mariadb-bin.NNNNNN 행이 모두 있어야 합니다. 마지막 일반 백업은 PITR 보조 체인까지 갖춘 새 기준입니다. 이후에는 --legacy-base를 사용하지 마세요.
복구 디렉터리는 다음 파일을 한 세트로 가집니다.
| 파일 | 내용 |
|---|---|
database.sql.gz |
설정된 MediaWiki DB 전체 논리 덤프 |
database-binlogs.tar.gz |
일반 백업만: 캡처 시점까지 보존된 MariaDB ROW binary log·index 보조 아카이브 |
pitr-manifest.txt |
일반 백업만: 기준 dump 뒤 replay를 시작할 binary-log file·position·GTID, archive 목록·digest |
mediawiki-images.tar.gz |
업로드·근거 볼륨의 numeric-owner 아카이브 |
mediawiki-config.tar.gz |
생성된 설정과 위키 키의 numeric-owner 아카이브 |
runtime-images.txt |
일반 백업만: 설정 reference와 실제 실행 컨테이너 image ID |
repository-tracked.patch.gz |
일반 백업만: HEAD에 적용할 추적 소스 binary patch. untracked·ignored 파일 제외 |
repository-tracked-manifest.txt |
일반 백업만: 추적 파일별 SHA-256과 소스 상태 |
metadata.txt |
형식 버전, DB 이름, 페이지·revision 수, 소스 revision, 이미지 참조, Compose 해시. 일반 백업은 PITR position도 포함 |
SHA256SUMS |
앞의 네 payload checksum |
AUXILIARY-SHA256SUMS |
일반 백업만: PITR·image·tracked-source 보조 파일 checksum. 이 파일 digest는 metadata.txt를 통해 기본 manifest와 결합
|
기본 대상, 각 복구 디렉터리, 모든 파일의 권한은 0700 또는 0600입니다. 기존 대상에 group/other 접근 비트가 하나라도 있으면 스크립트가 거부하며 권한을 임의로 바꾸지 않습니다. 백업할 때 db는 실행 중이어야 합니다. 위키가 원래 정지 상태였다면 그대로 두고, 실행 중이었다면 API가 다시 healthy가 될 때까지 기다린 뒤 성공을 알립니다. 복원도 소스 디렉터리에 같은 권한 검사를 합니다.
기본 로컬 대상에 저장하려면 경로를 생략합니다.
docker compose stop mcp-http ./scripts/backup.sh docker compose up -d mcp-http
기본 로컬 경로만 장기 보관에 사용하지 마세요. 완료된 디렉터리 전체를 접근이 제한된 외부 저장소에 복사합니다. 공개 메모가 주 내용이더라도 백업에는 사용자 정보, 삭제·이전 revision, 생성된 비밀 키가 있으므로 공개하지 않습니다.
백업 확인
스크립트가 실패하면 그 백업은 사용할 수 없습니다. 성공한 뒤에도 다음을 확인합니다.
- DB 덤프, 설정·이미지 아카이브, 메타데이터,
SHA256SUMS가 있는지 확인합니다. 일반 백업은 binary-log·PITR 보조 파일 전체와AUXILIARY-SHA256SUMS도 있어야 합니다. 최초--legacy-base는 보조 파일이 하나도 없어야 합니다. - 스크립트가 출력한 checksum 명령을 실행합니다. ```bash (cd /ABS/PATH/TO/BACKUP-DIRECTORY && \ sha256sum --check --strict SHA256SUMS) # 일반 PITR 백업에서만 추가 확인 (cd /ABS/PATH/TO/BACKUP-DIRECTORY && \ sha256sum --check --strict AUXILIARY-SHA256SUMS) ```
- 빈 파일이 없는지, 시각과 소스 버전이 맞는지 확인합니다.
- 디렉터리 전체를 한 단위로 복사합니다. 서로 다른 시각의 파일을 섞지 마세요.
- 아래의 격리 복원 시험을 정기적으로 실행합니다. checksum은 전송 무결성만 확인하며 실제 복원 가능성을 보장하지 않습니다.
MediaWiki 백업 안내도 DB와 파일을 함께 보관하도록 안내합니다. XML 페이지 내보내기는 보조 이동본으로 쓸 수 있지만 사용자 계정, 모든 로그, 업로드 메타데이터, 사이트 전체 상태를 담지 않으므로 이 복구 세트를 대신할 수 없습니다.
MariaDB PITR 경계
database.sql.gz는 완전 기준 백업입니다. pitr-manifest.txt의 binlog_file·binlog_position은 그 덤프가 이미 포함한 마지막 경계이자 replay 시작 좌표입니다. 이 좌표 이전 event를 다시 적용하면 중복 변경이 생깁니다. 이후 시각으로 이동하려면 기준 좌표 이후의 연속된 binary log가 전부 있어야 합니다. 백업 당시 archive에는 아직 생기지 않은 미래 event가 들어 있을 수 없으므로, 더 늦게 만든 복구 세트의 database-binlogs.tar.gz나 별도 연속 보관본이 필요합니다. 로컬 db_binlogs는 기본 14일 후 자동 만료되므로 원격 백업과 복원 시험이 이 기간 안에 성공해야 합니다.
PITR은 운영 DB에 바로 재생하지 마세요. 격리 복원을 먼저 완료한 뒤, mariadb-binlog --start-position에는 manifest의 좌표 다음 event부터 적용되도록 경계를 확인하고, --stop-datetime 또는 --stop-position을 명시하여 SQL을 만듭니다. 재생 전에 시작·종료 file, position, target UTC, 예상 event 수를 작업 기록에 남기고, Lesson·revision 개수와 대표 페이지를 확인합니다. binary log가 하나라도 빠졌거나 기준 position보다 이전 백업과 섞였다면 PITR를 계속하지 말고 다음 완전 백업으로 복원합니다.
권장 주기
파일럿에서는 다음 주기로 시작합니다.
- 매일 백업, 14일 보관
- 매주 백업, 8주 보관
- 이미지·의존성 업데이트나 자격 증명 변경 전 백업
- 분기마다 격리 복원 시험
실제 복구 시점 목표는 운영 상황에 맞게 정합니다. 스케줄러 종료 상태와 디스크 여유 공간을 확인하세요. 오류 알림이 없는 예약 명령만으로는 충분하지 않습니다. 새 복원을 확인하기 전에 가장 최근의 정상 백업을 지우지 마세요.
Cloudflare R2 자동 백업
이 서버는 다음 비공개 R2 bucket의 restic prefix를 원격 백업 저장소로 사용합니다.
https://96f01030a087d07d906bff299d260155.r2.cloudflarestorage.com/s3rm-backups
백업 데이터는 restic이 서버에서 먼저 암호화한 뒤 전송합니다. R2 Access Key, Secret Key와 restic 암호는 Git이나 프로젝트 .env에 넣지 않습니다.
서버에 restic을 설치한 뒤 root 권한으로 설정 파일을 준비합니다.
sudo install -d -m 0700 /etc/s3-research-memory /srv/s3rm-backups sudo install -m 0600 config/remote-backup.env.example \ /etc/s3-research-memory/remote-backup.env sudo sh -c 'umask 077; openssl rand -base64 48 > /etc/s3-research-memory/restic-password' sudoedit /etc/s3-research-memory/remote-backup.env
R2에서 s3rm-backups bucket에만 목록 조회·읽기·쓰기·삭제가 가능한 S3 API 자격 증명을 만들고 설정 파일의 두 placeholder를 바꿉니다. 삭제 권한은 오래된 restic snapshot과 pack을 보존 정책에 맞게 정리할 때 필요합니다. restic 암호 파일은 서버 장애 때도 찾을 수 있도록 서버 밖의 비밀번호 관리자에 따로 보관합니다. 이 암호를 잃으면 R2의 백업을 복구할 수 없습니다.
현재 live restic repository의 자격 증명은 forget·prune를 위해 삭제 권한도 가집니다. 따라서 R2 자격 증명 탈취와 오작동으로부터의 불변 백업은 현재 저장소 하나만으로 달성되지 않습니다. 이 제한은 복원 시험 기록에 항상 남깁니다.
R2의 lock·retention을 live restic prefix에 바로 적용하면 임시 lock 제거와 forget·prune가 실패할 수 있습니다. 삭제 방어는 다음과 같이 별도 보호 복사본으로 구성합니다.
- live
resticprefix는 현재 보존 정책으로 검증·정리합니다. - 검증한 복구 세트 또는 restic repository 세대를 별도 bucket·account의 보호 prefix로 복사합니다. 복사 writer와 retention 변경 권한을 분리합니다.
- 보호 복사본은 일간 운영 키로 삭제할 수 없게 만들고, 예외 해제 권한은 다른 계정에 보관합니다.
- Cloudflare 계정 MFA, bucket 범위 API token, 삭제·retention 변경 알림, 분기별 보호 복사본 복원 시험을 함께 적용합니다.
별도 bucket/retention/API token은 Cloudflare 외부 설정이므로 이 저장소의 Compose나 systemd 파일만으로는 활성화할 수 없습니다. 이 설정이 완료되기 전에는 live R2를 유일한 삭제 방어 본으로 간주하지 마세요.
원격 저장소는 한 번만 초기화합니다.
sudo bash -c ' set -a source /etc/s3-research-memory/remote-backup.env set +a exec /home/mrcha033/Projects/s3wiki/scripts/init-remote-backup.sh '
systemd 단위를 설치하고 첫 백업을 수동으로 확인합니다.
sudo install -m 0644 deploy/systemd/s3rm-remote-backup.service \ /etc/systemd/system/s3rm-remote-backup.service sudo install -m 0644 deploy/systemd/s3rm-remote-backup.timer \ /etc/systemd/system/s3rm-remote-backup.timer sudo systemctl daemon-reload sudo systemctl start s3rm-remote-backup.service sudo systemctl status s3rm-remote-backup.service sudo systemctl enable --now s3rm-remote-backup.timer systemctl list-timers s3rm-remote-backup.timer
작업은 매일 서울 시각 03:15 이후 45분 안에 실행합니다. 성공한 원격 snapshot은 일간 14개와 주간 8개를 보존하고, 원격 pack 정리는 일요일에 실행합니다. 전송이나 검증이 실패하면 로컬 복구 세트를 지우지 않습니다. 성공하면 R2에 이미 암호화된 snapshot을 확인한 뒤 /srv/s3rm-backups의 해당 임시 세트를 정리합니다. snapshot 경로에는 매번 다른 시각이 들어가므로 restic 보존 그룹은 host,tags로만 묶습니다. 경로까지 그룹 기준에 넣으면 각 snapshot이 항상 자신만의 그룹이 되어 14일·8주 정책이 아무 것도 만료시키지 못합니다.
상태와 최근 로그를 정기적으로 확인합니다.
systemctl status s3rm-remote-backup.timer sudo journalctl -u s3rm-remote-backup.service --since '2 days ago'
원격 snapshot을 복구할 때는 빈 root 전용 디렉터리에 먼저 내려받고 기존 복원 스크립트로 넘깁니다.
sudo install -d -m 0700 /srv/s3rm-remote-restore sudo bash -c ' set -a source /etc/s3-research-memory/remote-backup.env set +a restic snapshots --tag s3rm-recovery-set restic restore latest --tag s3rm-recovery-set --target /srv/s3rm-remote-restore ' sudo find /srv/s3rm-remote-restore -maxdepth 2 -type f -name SHA256SUMS -print
출력된 SHA256SUMS의 상위 디렉터리가 한 개이고 원하는 시각의 복구 세트인지 확인한 뒤 scripts/restore.sh <그 디렉터리> --yes를 사용합니다. 운영 서버를 덮어쓰기 전에 격리 복원 시험을 먼저 수행합니다.
Lesson 내용 정기 감사
읽기 전용 감사 작업은 매주 운영 위키의 안정된 Lesson을 중앙 mcp-http에서 읽습니다. 실행 전 30분 안에 바뀐 Lesson은 다음 감사로 미룹니다. 별도 Lesson 저장소나 기준 파일을 만들지 않습니다. 쓰기 토큰도 사용하지 않습니다. 외부 근거 URL을 열거나 Lesson을 자동으로 수정하지 않습니다.
감사 작업은 다음 항목을 확인합니다.
- 구조화된 근거가 없는 Lesson
highconfidence인데 최신 append-only 검증 기록이 observation, interpretation, reusable_lesson 전부를 strongsupports로 뒷받침하지 않는 Lesson- 확인 범위가
not_recorded인 근거 - 같거나 거꾸로 된
verified_at으로 append 순서의 현재 판정을 정할 수 없는 Lesson - 서로 같은 관찰·해석 또는 재사용 내용·적용 조건
- 대상이 없는 relation과 중복 relation
- 365일 이상 바뀌지 않은 검토 후보
- 비밀 키나 토큰 형식으로 보이는 문자열
발견 결과에는 Lesson ID, revision ID, codes 배열과 상한이 있는 evidence·verification ID만 남깁니다. Lesson 본문, 인용과 relation note는 journal에 남기지 않습니다. 같은 Lesson의 발견 코드는 한 줄로 묶습니다. 오래된 Lesson이라는 이유만으로 틀렸다고 판정하지 않으며 stale_review_candidate로만 표시합니다. 감사는 저장된 구조, digest, append 순서와 검증 수령 기록의 일관성을 확인합니다. 외부 근거 URL을 열지 않으므로 논문·코드·데이터의 사실 진위를 자동으로 판정하지 않습니다.
systemd 단위를 설치하고 첫 감사를 수동으로 실행합니다.
sudo install -D -m 0755 scripts/audit-lessons.py \ /usr/local/libexec/s3rm-content-audit sudo install -m 0644 deploy/systemd/s3rm-content-audit.service \ /etc/systemd/system/s3rm-content-audit.service sudo install -m 0644 deploy/systemd/s3rm-content-audit.timer \ /etc/systemd/system/s3rm-content-audit.timer sudo systemctl daemon-reload sudo systemctl start s3rm-content-audit.service sudo systemctl status s3rm-content-audit.service sudo journalctl -u s3rm-content-audit.service --since today sudo systemctl enable --now s3rm-content-audit.timer systemctl list-timers s3rm-content-audit.timer
작업은 일요일 서울 시각 04:30 이후 30분 안에 실행합니다. 놓친 실행은 서버가 다시 켜졌을 때 실행합니다. 배포 단위와 make audit-lessons는 현재 서버의 scan limit과 같은 500개 Lesson을 읽습니다. 스크립트 자체의 기본값은 200개이고 설정 가능한 절대 상한은 1000개입니다. Lesson당 근거·relation은 각각 최대 200개를 읽습니다. 서버의 scan_limit에서 목록이 잘리거나 감사 중 revision과 Lesson 목록이 바뀌면 완료로 기록하지 않습니다. 연결 오류나 동시 변경으로 끝나면 5분 간격으로 최대 세 번 시도합니다. 발견 사항을 뜻하는 종료 코드 2는 다시 시도하지 않습니다.
종료 코드는 다음과 같습니다.
| 코드 | 뜻 | 다음 작업 |
|---|---|---|
0 |
감사를 완료했고 발견 사항이 없음 | 다음 예약 실행을 기다립니다. |
1 |
연결, 출력 한도 또는 동시 변경으로 감사를 완료하지 못함 | journal의 마지막 오류를 확인하고 다시 실행합니다. |
2 |
감사를 완료했고 확인할 항목이 있음 | 발견된 Lesson을 읽고 근거를 확인합니다. |
systemd 서비스는 임시 계정으로 실행합니다. 홈 디렉터리, .env, Docker 소켓과 /etc/s3-research-memory를 읽을 수 없습니다. 발견 사항이 있으면 실패 상태로 표시합니다. 이 상태는 Lesson을 바꾸지 않으며 다음 예약 실행도 막지 않습니다. 문제를 고칠 때는 근거를 먼저 붙인 뒤 같은 Lesson ID에 현재 revision을 기준으로 새 revision을 추가합니다. supersedes나 근거 note를 기존 주장 수정 수단으로 쓰지 않습니다.
예약과 무관하게 현재 상태를 확인할 때는 다음 명령을 사용합니다. 발견 사항이 있어도 수동 명령은 감사 완료 뒤 코드 0을 반환합니다. 자동 점검과 같은 종료 코드가 필요하면 --fail-on-findings를 추가합니다.
make audit-lessons
복원 전 주의
scripts/restore.sh는 현재 DB, 업로드, 생성된 위키 설정을 교체합니다. 명시적인 --yes가 필요하며, 공지한 점검 시간에만 실행합니다. 병합이나 페이지 가져오기 도구가 아닙니다.
복원 전에 다음을 확인합니다.
- 정확한 백업 디렉터리를 정하고 모든 checksum을 확인합니다.
- 백업의 MediaWiki·애플리케이션 버전이 현재 checkout과 호환되는지 확인합니다. 새 DB를 더 오래된 코드에 복원할 수 없습니다.
- 점검 시간을 알리고
mcp-http를 멈춘 뒤 브라우저 편집도 중단합니다. - 현재 상태를 읽을 수 있으면 복원 직전 백업을 하나 더 만듭니다.
.env에 복원할 인스턴스와 맞는 자격 증명이 있는지 확인합니다.- 복원을 결정한 사람과 이유를 기록합니다.
현재 서버 복원
백업을 만든 것과 같은 저장소 버전에서 실행합니다.
docker compose --profile domain stop caddy mcp-http ./scripts/restore.sh /ABS/PATH/TO/BACKUP-DIRECTORY --yes
스크립트는 checksum manifest, 백업 형식 버전, 압축 stream, 모든 아카이브 member의 경로와 종류를 먼저 확인합니다. 보조 파일이 있는 형식은 auxiliary manifest digest와 binary-log·source artifact도 모두 확인합니다. 백업의 DB 이름과 MARIADB_DATABASE도 같아야 합니다. 다르면 덤프를 편집하지 말고 복구용 .env를 맞춥니다. 이 검증과 이미지 빌드 사전 확인이 모두 끝난 뒤에만 애플리케이션을 멈추고 기준 DB·설정·업로드를 교체합니다.
기준 dump import는 sql_log_bin=0 세션에서 실행합니다. 이어서 DB를 멈추고 기존 db_binlogs의 더 새로운 운영 timeline을 비운 뒤 새 local timeline으로 시작합니다. 따라서 과거 DB에 미래 binlog가 이어지는 일은 없지만, backup의 archived binlog를 자동 replay하지도 않습니다. PITR이 필요하면 운영 복원 전에 별도 격리 환경에서 목표 UTC·종료 position을 정해 검증한 절차로만 적용합니다.
mediawiki 시작 때 MediaWiki·확장 기능 마이그레이션을 적용하고, 이어서 반복 실행이 가능한 bootstrap을 적용합니다. 그 뒤 mediawiki-job-state-init을 강제로 다시 실행해 새 복구 볼륨을 UID/GID 33 쓰기 경로로 만든 다음 bounded mediawiki-jobs를 시작해 worker health까지 확인합니다. 복구 중 MediaWiki host bind는 임시로 loopback에 강제하고 caddy와 mcp-http를 성공 후에도 중지한 상태로 둡니다. 따라서 마이그레이션· bootstrap 중 공개 브라우저나 MCP 쓰기가 재개되지 않습니다. 교체 경고가 나온 뒤에는 중단하지 마세요. 오류가 나오면 클라이언트를 멈춘 상태로 로그와 소스 백업을 보존합니다. 실패한 상태 위에 부분 SQL이나 일부 볼륨을 임의로 덮어쓰지 마세요.
완료 뒤 상태를 확인합니다.
docker compose ps -a docker compose logs mediawiki bootstrap mediawiki-jobs
아래 MediaWiki 복구 확인을 마칠 때까지 mcp-http를 멈춰 둡니다. 확인 뒤 현재 설정으로 공용 어댑터를 다시 만들고, 공개 조회와 토큰 기반 변경을 모두 시험합니다.
docker compose --profile domain up -d --force-recreate mediawiki mcp-http caddy docker compose ps mediawiki-jobs mcp-http docker compose logs --tail=100 mcp-http ./scripts/verify-public-domain.sh s3wiki.yonsei.ac.kr
격리 복원 시험
실제 서버를 덮어쓰지 말고 별도 Compose 프로젝트와 사용하지 않는 포트에서 시험합니다. 별도 checkout을 사용하고, 복구 디렉터리는 이동하지 말고 복사합니다. Compose 프로젝트와 볼륨 이름을 고유하게 지정하세요. 최상위 이름은 COMPOSE_PROJECT_NAME으로 바꿀 수 있으며 두 스크립트가 이 값을 사용합니다. COMPOSE 환경 변수에는 docker compose --ansi never처럼 고정 접두사를 넣을 수 있습니다. 설치, 백업, 복원에 같은 값을 사용하세요.
권장 경로는 전용 검증 스크립트입니다. 운영 .env가 아닌 mode 0600 일회용 env에 사용하지 않는 loopback 포트, 일회용 DB·wiki 비밀번호, MW_SERVER_URL=http://127.0.0.1:18080과 S3RM_WIKI_BIND_ADDRESS=127.0.0.1, S3RM_HTTP_PORT=18080을 넣습니다. 복원용 S3RM_WIKI_BOT_PASSWORD도 openssl rand -hex 16으로 따로 만들고, 운영 BotPassword와 primary 위키 비밀번호를 재사용하지 않습니다. 검증 스크립트는 DB를 가져온 직후 격리 project에서만 복원된 관리자 비밀번호를 일회용 env의 관리자 secret으로 바꿉니다. secret은 명령행 인자나 로그에 넣지 않으며, 격리 환경을 지울 때 변경된 관리자 해시도 함께 없어집니다. 운영 project에는 이 비밀번호 재설정 옵션을 사용할 수 없습니다. 실제 Compose project 이름과 모든 volume·network 이름도 함께 검사하므로 COMPOSE 접두사는 검증 스크립트가 만든 고정 명령만 허용합니다. 따라서 다른 --project-name, Compose override, bind mount, 외부 volume 또는 별칭을 덧붙여 이 경계를 우회할 수 없습니다. bootstrap의 관리자·MCP 기본 계정·BotPassword도 secret 파일에서 직접 읽으며 평문을 자식 프로세스 환경이나 명령행 인자로 전달하지 않습니다.
chmod 0600 /srv/s3rm-restore-test.env ./scripts/verify-restore-isolated.sh /ABS/PATH/TO/BACKUP-DIRECTORY \ --env-file /srv/s3rm-restore-test.env \ --project-name s3rm-restore-2026q3
스크립트는 project 접두사, 새 project에 기존 컨테이너·볼륨·네트워크가 없는지와 resolved Compose가 사용할 실제 volume·network 이름도 모두 비어 있는지, loopback 주소, 기본 checksum과, 있는 경우 PITR 보조 checksum 체인을 변경 전 확인합니다. 격리 project 이름으로 전용 MediaWiki image tag를 만들어 운영 image tag를 동시에 다시 빌드하지 않습니다. 복원 후 Lesson 개수는 정확히 일치해야 하고, revision은 bootstrap이 관리 페이지 변경을 추가할 수 있으므로 백업 기록 이상이어야 합니다. MediaWiki API, bootstrap, bounded mediawiki-jobs health도 확인합니다. 성공하면 격리 볼륨을 지우고, 실패하면 진단을 위해 보존합니다. 성공 시 project 전용 MediaWiki image tag도 함께 지웁니다. 자동 정리가 실패하면 스크립트가 실패 상태와 정확한 수동 정리 명령을 출력합니다. --keep을 사용하면 성공한 환경도 보존합니다. 이 검증은 스키마·개수·시작 경계를 자동 확인하지만, 아래의 사람이 열어 보는 대표 Lesson·업로드·검색 확인을 대신하지 않습니다.
Docker가 설치된 일회용 VM에서는 다음 순서가 안전합니다.
- 백업과 정확히 같은 저장소 revision, 복구 디렉터리, 복구용
.env를 VM에 복사합니다. COMPOSE_PROFILES=로 실제 hostname과 80/443을 사용하지 않게 합니다. 겹치지 않는S3RM_HTTP_PORT와 맞는 로컬MW_SERVER_URL을 설정합니다. 별도 loopbackS3RM_MCP_HTTP_PORT·S3RM_MCP_PUBLIC_URL과 일회용 MCP 토큰도 사용합니다. 실제 MCP URL이나 토큰을 복원 시험에 쓰지 마세요.- VM에서
scripts/restore.sh <backup-directory> --yes를 실행합니다. - 아래 복구 확인을 모두 수행합니다.
- 결과를 기록한 뒤 VM을 지웁니다. 원본 복구 세트는 그대로 둡니다.
docker compose config와 docker volume ls에서 볼륨 이름이 다른지 보기 전에는 COMPOSE_PROJECT_NAME만 바꿨다고 격리된 것으로 보지 마세요.
복구 완료 확인
다음 항목을 모두 통과해야 복원이 끝납니다.
db,mediawiki,mediawiki-jobs가 healthy이고mediawiki-install과bootstrap이 코드 0으로 종료되었습니다.Special:Version에 예상한 MediaWiki, Semantic MediaWiki, Page Forms, S3 Research Memory 버전이 표시됩니다.- Lesson 페이지 수와 최근 revision 수가 백업 기록과 맞습니다.
- 복원된 Lesson의 필드, 근거, append-only 검증 기록, 관계, 출처, 호환 필드, 시간이 정상 표시됩니다.
- 역사 보기에서 이전 revision 하나 이상이 열립니다.
- 업로드한 근거가 열리고, 외부 checksum을 기록했다면 값이 맞습니다.
- 전문 검색이 알려진 제목과 본문 단어를 찾습니다.
- 격리한 복원 시험 환경에서만 일반 브라우저 계정으로 Lesson 양식을 열고 복구 확인용 메모를 저장합니다. 바로 검색되는지 본 뒤 격리 환경과 함께 폐기합니다.
- 다시 만든
mcp-http가 healthy입니다. 토큰 없이 초기화·도구 목록·search_lessons를 실행하면 짧은 인용 결과가 나옵니다. 쓰기 토큰을 넣은 클라이언트는 같은 격리 환경에 확인용 메모를 만들 수 있습니다. 운영 위키에는 확인용 데이터를 만들지 않습니다. - 따로 설정한 두 번째 클라이언트가 같은 Lesson을 조회합니다. 두 클라이언트가 로컬 데이터가 아닌 복원된 위키 하나를 보고 있어야 합니다.
- MCP 계정은 여전히 삭제, 이동, 보호, 관리 기능을 사용할 수 없습니다.
복구 디렉터리, 소요 시간, 애플리케이션 revision, 결과, 실행자, 필요했던 조치를 기록합니다. 하나라도 실패하면 점검 상태를 유지합니다.
평소 상태 확인
상태와 제한된 길이의 로그를 확인합니다.
docker compose ps -a docker compose logs --tail=200 db mediawiki-install mediawiki bootstrap \ mediawiki-jobs mcp-http caddy
로컬 프로세스 경로와 두 공개 경로를 차례로 확인합니다. 로컬 확인만으로 다른 PC의 HTTPS 접속을 대신할 수 없습니다.
curl --fail --silent --show-error \
'http://localhost:8080/api.php?action=query&meta=siteinfo&format=json' \
>/dev/null
curl --fail --silent --show-error \
'http://127.0.0.1:8765/healthz' \
| jq --exit-status \
'.canonical_store == "ok" and
.job_runner == "ok" and
.write_admission.active >= 0 and .write_admission.waiting >= 0' \
>/dev/null
curl --fail --silent --show-error \
'https://s3wiki.yonsei.ac.kr/api.php?action=query&meta=siteinfo&format=json' \
>/dev/null
setup.md의 토큰 없는 MCP 초기화도 https://s3wiki.yonsei.ac.kr/mcp에서 실행합니다. 단순 GET /mcp는 streaming 연결을 계속 열어 둘 수 있으므로 상태 확인이 아닙니다.
같은 서버에서 위 확인을 한 번에 실행하려면 make verify-domain을 사용합니다. 이 결과만으로 학교 밖 인바운드 경로까지 확인되지는 않습니다.
Compose 운영의 /healthz는 canonical store와 bounded job runner가 모두 준비돼야 200을 반환합니다. job_runner=unavailable이면 공유 상태 파일의 generation·최근 성공 시각·권한을 먼저 확인합니다. write_admission에는 비밀값 없이 현재 active·waiting·principal 수와 동시 실행·queue 상한이 나옵니다. 기본은 전체 동시 쓰기 8개, queue 64개·2초, principal별 지속 600회/분·burst 120입니다. open 모드에서는 client IP가 principal이므로 인증 모드를 바꾸지 않아도 사용자별 admission이 적용됩니다. 지표가 계속 상한에 붙거나 server_busy가 반복되면 DB latency와 실패 요청을 먼저 확인하고, 변경한 상한은 mcp-http를 다시 만들어 적용합니다. 이 queue는 재시작 뒤 작업을 복구하는 durable queue가 아닙니다. 전달된 IP는 Caddy와 MCP의 S3RM_MCP_PROXY_SECRET이 맞을 때만 신뢰합니다. 이 값을 교체할 때는 둘을 같은 Compose 재생성에서 함께 전환합니다.
MediaWiki job queue는 전용 mediawiki-jobs 서비스가 기본 25건·20초, 1 process로 처리합니다. 상태 파일은 비밀 값이 없고 현재 queue 깊이, 마지막 성공 시각, 연속 실패 수, batch/idle 상한과 현재 runner generation을 담습니다. generation은 컨테이너 PID 1의 시작 시각에서 만들므로 이전 컨테이너나 복구 전 상태 파일은 새 runner health로 인정되지 않습니다. 상태 볼륨은 mediawiki-job-state-init이 UID/GID 33으로 준비하고 MCP에는 읽기 전용으로만 연결합니다.
docker compose ps mediawiki-jobs docker compose ps -a mediawiki-job-state-init docker compose exec mediawiki-jobs cat /run/s3rm-jobs/status.env docker compose exec mediawiki-jobs /usr/local/bin/s3-mediawiki-jobs-health docker compose exec mediawiki \ php maintenance/run.php showJobs --group
runner가 실패하면 15초에서 시작해 최대 5분까지 지수 backoff합니다. 마지막 성공이 기본 120초보다 오래되면 health가 실패합니다. queue를 빠르게 비우기 위해 --nothrottle이나 다중 process를 임의로 사용하지 마세요. 먼저 실패 job 종류와 DB·디스크 상태를 확인하고, 필요한 처리량만 .env에서 조정합니다.
Compose는 각 주요 서비스의 json-file 로그를 파일당 10MB, 최대 5개로 회전하고, PID·메모리·CPU 상한을 둡니다. 평소 사용량과 OOM·재시작 여부를 함께 확인합니다.
docker stats --no-stream
docker inspect --format \
'{{.Name}} oom={{.State.OOMKilled}} restarts={{.RestartCount}}' \
$(docker compose ps -q)
docker system df
상한 도달은 health check 성공만으로 보이지 않을 수 있습니다. OOM이나 PID 부족을 확인한 뒤 queue·동시성·실패 loop 원인을 먼저 고치고, 측정한 정상 peak와 호스트 여유를 근거로 상한을 조정합니다. 로그 회전 파일은 장기 감사 자료가 아니므로 필요한 운영 event는 별도 제한된 저장소에 보관합니다.
Special:RecentChanges, 계정 그룹, 로그인 실패, MCP 쓰기 인증 실패와 403/421, 디스크 여유 공간, 백업 시각, 복원 시험 상태를 확인합니다. bootstrap이 관리하는 스키마·양식 페이지가 반복해서 달라지면 원인을 찾으세요. 일반 Lesson revision은 bootstrap이 바꾸지 않습니다.
공용 MCP 운영
mcp-http에는 영구 지식이 없습니다. 정지하거나 다시 만들면 진행 중인 도구 호출이 끊기고 클라이언트가 재연결해야 하지만 Lesson이나 revision은 없어지지 않습니다.
docker compose stop mcp-http docker compose up -d mcp-http docker compose up -d --force-recreate mcp-http docker compose ps mcp-http
일시적인 프로세스 오류에는 restart를 사용합니다. 토큰 해시, 공개 URL, bind·port, allowlist, 위키 자격 증명, 이미지를 바꿨다면 --force-recreate를 사용합니다. 공개 mcp-http 복제본을 여러 개 띄우지 마세요. 현재 서비스에는 load balancer나 session 공유 설계가 없습니다. 읽기 요청은 재연결 뒤 다시 시도해도 됩니다. 시간이 초과된 변경 요청은 결과를 먼저 확인한 뒤 다시 보내세요.
공개 HTTPS
Caddy는 Compose domain 프로필에서 실행하며 공개 포트를 사용합니다.
docker compose ps caddy docker compose logs --tail=200 caddy docker compose up -d --force-recreate caddy make verify-domain
S3RM_PUBLIC_HOST나 저장소의 Caddyfile을 바꾼 뒤 Caddy를 다시 만듭니다. 인증서 발급을 다시 시도하려고 caddy_data를 지우지 마세요. 먼저 s3wiki.yonsei.ac.kr이 165.132.118.220을 가리키는지, 다른 프로세스가 80/443을 쓰는지, 두 포트가 외부에서 서버까지 들어오는지 확인합니다. Caddy는 원래 Host 헤더를 보존하며 MCP는 이를 S3RM_MCP_PUBLIC_URL=https://s3wiki.yonsei.ac.kr/mcp와 대조합니다. 학교 밖 네트워크의 브라우저 확인도 별도로 해야 합니다.
공용 MCP 토큰 교체
이 파일럿은 토큰 해시 하나만 받으므로 교체 시 모든 쓰기 클라이언트의 기존 토큰이 한 번에 무효가 됩니다. 공개 조회는 계속됩니다. 변경이 잠시 끊긴다고 알리고 새 서비스 확인이 끝날 때까지 기존 client.env를 보관하세요.
- 원문을 출력하지 않고 새 토큰과 해시를 만듭니다. ```bash ./scripts/configure-mcp-token.sh --rotate ``` 스크립트는
.env의 해시만 바꾸고, 원문 토큰과 URL은 mode0600~/.config/s3-research-memory/client.env에 씁니다.--rotate가 없으면 기존 토큰과 해시가 맞는지 확인하고 그대로 둡니다. - 공용 어댑터만 다시 만듭니다. ```bash docker compose up -d --force-recreate mcp-http docker compose ps mcp-http docker compose logs --tail=100 mcp-http ```
- 토큰 없이 초기화, 도구 목록,
search_lessons(limit=1)을 확인합니다. 이어서 신뢰하는 셸에서 새 비공개 파일을 불러오고setup.md의 쓰기 확인을 실행합니다. - 새 원문 토큰 또는 클라이언트 환경을 쓰기 클라이언트에 전달하고 재연결합니다. 공개 조회 클라이언트는 바꿀 것이 없습니다. DB·위키 비밀번호와 쓸 수 없는 토큰 해시가 든 서버
.env는 전달하지 마세요.
새 서비스를 확인하기 전에 문제가 생겼다면 이전 해시와 그에 맞는 비공개 토큰을 복구하고 mcp-http를 다시 만듭니다. 변경 인증을 끄는 방식으로 되돌리지 마세요.
애플리케이션 업데이트와 되돌리기
업데이트 전에 이름을 붙인 백업을 만들고 복원을 시험합니다. 새 이미지를 빌드하고 DB 마이그레이션과 bootstrap을 완료한 뒤 복구 완료 항목을 확인합니다. 새 release가 DB를 마이그레이션했다면 이미지만 낮춰서는 안 됩니다. 이전 저장소 revision과 함께 업데이트 전 DB, 업로드, 설정을 한 세트로 복원해야 합니다.