서버가 돌아오지 않아도 사이트를 읽을 수 있게 만들기
개요
운영 서버가 네트워크에서 사라졌다.
웹툰 검색, 셀럽인사이트, 블로그가 함께 영향을 받았다. 공개 사이트는 오류를 반환했고, 운영 서버에는 접속할 수 없었다. 현장에 가서 전원 버튼을 눌러볼 수도 없는 상황이었다.
다행히 Windows에 백업은 있었다. 그래서 처음에는 이렇게 생각했다.
“백업으로 복구하면 되는 거 아닌가?”
맞는 말이었다. 다만 그 문장 안에 백업 검증, DB 복원, 공개 경로 전환, 권한 제한, 브라우저 테스트가 압축돼 있었을 뿐이다. 압축률이 상당했다.
이번 글에서는 세 사이트를 백업으로 복구하고, 이후에는 같은 과정을 자동으로 수행하도록 구성한 경험을 정리하려 한다. 백업 파일을 보관하는 일에서, 실제로 서비스할 수 있는 복구본을 준비하는 일로 넘어간 과정이다.
1. 코드 밖에서 시작된 장애
처음 확인한 상태는 단순했다.
개발 서버는 응답했지만 운영 서버는 No route to host를 반환했다. Tailscale에서도 운영 서버가 보이지 않았고, 공개 사이트는 HTTP 530 오류를 보여주고 있었다.
플러그를 뽑았다가 전원을 다시 켜지 않은 것 같다는 가능성이 있었다. 다만 현장 확인을 하지 못했으므로 전원 문제라고 확정할 수는 없었다.
확인한 사실은 운영 서버에 연결할 수 없다는 것까지였다.
이 차이가 꽤 중요하다고 생각한다. 원인을 추측하는 것과 확인하는 것은 별개다. 전원 문제를 확신한다고 해서 원격으로 전원 버튼이 눌리는 것도 아니었다.
당시 서버 역할은 다음처럼 나뉘어 있었다.
| 구분 | 역할 |
|---|---|
| A 서버 | 개발·빌드·배치와 백업·장애 제어 |
| B 서버 | 공개 앱과 운영 DB |
| Windows | 암호화된 백업 저장소 |
이 구조 덕분에 B가 응답하지 않아도 A에서 Windows 백업을 확인할 수 있었다. 복구에 필요한 실행 환경과 데이터가 모두 장애 서버 안에 갇힌 상황은 피한 셈이다.
우선 목표를 정했다.
B의 물리 상태를 확인할 수 없는 동안에도, 사용자가 기존 콘텐츠를 읽을 수 있게 만들자.
2. 백업이 있다는 사실부터 다시 검증했다
양심고백을 하자면, 백업 파일이 있다는 말을 들으면 마음이 조금 놓인다. 파일 크기가 몇 GB쯤 되면 더욱 그렇다. 뭔가 많이 들어 있으니 든든해 보인다.
하지만 파일 크기는 복구 가능성을 설명해 주지 않는다.
이번에는 Windows의 restic 저장소에 있는 백업을 대상으로 다음을 확인했다.
- 어느 서버의 어떤 서비스를 백업한 스냅샷인지 확인한다.
- 해당 스냅샷의 내보내기 파일 전체를 다시 읽는다.
- 바이트 수와 SHA-256을 기존 증빙과 비교한다.
- 압축 내부 파일 목록과 개별 해시를 검증한다.
- SQLite와 PostgreSQL을 서비스별 방식으로 검증한다.
- 복구한 앱을 실행해 실제 기능을 확인한다.
파일을 찾았다는 이유만으로 복구본을 준비 완료 상태로 올리지 않았다.
백업 결과를 읽는 방식도 신경 써야 했다.
일일 백업은 여러 서비스를 순서대로 처리하고 있었다. 앞선 서비스 백업은 성공했는데 마지막 단계가 실패하면, 전체 실행 결과는 실패로 남을 수 있었다.
전체 결과의 failed만 보고 모든 백업을 버리면 이미 성공한 서비스 백업까지 놓친다. 반대로 일부 성공을 보고 전체가 안전하다고 판단해도 안 된다.
그래서 서비스별 완료 증빙을 따로 판단했다.
웹툰에는 일일 백업보다 나중에 만들어진 수동 보호본도 있었다. 이것 역시 파일명이나 수정 시각만 보고 선택하지 않았다. 복원 증빙이 가리키는 스냅샷과 Windows 저장소의 실제 스냅샷이 정확히 일치하는지 확인한 뒤 사용했다.
백업 목록에서 가장 위에 있는 파일을 고르는 일보다 복잡했다. 그래도 장애 상황에서 “아마 최신일 것 같다”에 기대고 싶지는 않았다.
3. 복구 범위는 읽기 전용으로 잡았다
A에는 과거 운영 흔적이 남아 있었다. 그렇다고 예전에 쓰던 앱과 DB를 그대로 켜는 것은 적절한 복구 방법이 아니었다.
현재 데이터 정본은 B에 있었다. A에 남은 과거 DB가 얼마나 뒤처져 있는지, B에서 그동안 어떤 쓰기가 발생했는지 확인하지 않고 다시 운영을 시작하면 문제가 복잡해진다.
나중에 B가 돌아왔을 때 이런 질문을 만나게 된다.
“그래서 어느 DB가 진짜지?”
장애 하나를 해결하려다가 정본이 두 개 생기는 상황은 피하고 싶었다.
이번에는 A의 별도 경로에 검증된 백업을 복원하고, 공개 기능을 읽기 전용으로 제공했다.
- 웹툰은 검색·회차·요약·등장인물 정보를 제공한다.
- 블로그는 공개 글·검색·목록을 제공한다.
- 셀럽인사이트는 인물 검색·프로필·인사이트를 제공한다.
- 관리자 기능과 사용자 데이터를 변경하는 요청은 차단한다.
화면에는 읽기 전용 복구 상태와 백업 기준 시각을 표시했다. 사용자가 현재 데이터를 보고 있는지, 특정 시점의 복구본을 보고 있는지 알 수 있어야 한다고 생각했다.
검수가 끝나지 않은 새 콘텐츠도 복구 과정에 끼워 넣지 않았다. 입력은 백업에 담긴 기존 공개 콘텐츠로 제한했다.
장애 복구가 밀린 배포를 처리하는 기회가 될 필요는 없었다. 그날 할 일은 이미 충분했다.
4. 읽기 전용이라는 말도 구현해 보면 단순하지 않다
처음에는 GET과 HEAD만 허용하면 읽기 전용에 가까워질 것처럼 보였다.
그런데 앱 안쪽을 보면 이야기가 달라진다.
예를 들어 조회 API가 요청 제한을 위해 SQLite에 카운터를 기록할 수 있다. 사용자 입장에서는 조회지만 서버 내부에서는 쓰기가 발생한다.
반대 상황도 있었다. 셀럽인사이트의 검색어 해석 API는 POST를 사용하지만, 공개 검색 조건을 해석하는 요청이었다.
HTTP 메서드만으로 실제 데이터 변경 여부를 판단하기는 어려웠다.
그래서 공개 라우트를 하나씩 확인하고 허용 목록을 만들었다. 내부에서 필요한 임시 상태는 복구본 전용 경로에 격리했다. 검색어 해석처럼 필요한 예외에는 요청 크기와 형식 제한을 붙였다.
DB 쪽에도 별도 제한을 걸었다.
셀럽인사이트의 PostgreSQL은 외부 포트를 공개하지 않는 격리 컨테이너에 복원했다. 앱은 Unix socket으로 연결하고, 공개 앱 계정에는 조회에 필요한 권한만 부여했다. 데이터와 스키마를 변경할 수 있는 권한, 위험한 함수 실행 권한도 확인했다.
프록시의 쓰기 차단과 DB 권한 제한을 함께 둔 것이다.
프론트엔드에서 버튼을 숨기는 것만으로 권한 제어가 끝나지 않는다는 사실은 익숙하다. 복구 화면도 마찬가지였다. 배너에 “읽기 전용”이라고 적는다고 DB가 그 문구를 읽고 협조해 주지는 않는다.
5. HTTP 200 다음에 사용자의 동선을 확인했다
앱 프로세스가 실행되고 health endpoint가 200을 반환하면 일단 안심하게 된다.
솔직히, 나도 예외는 아니었다.
하지만 사용자는 health endpoint를 보러 사이트에 들어오지 않는다. 웹툰 대사를 검색하고, 블로그 글을 열고, 인물 프로필을 확인하러 들어온다.
그래서 준비 완료 조건에 브라우저 검사를 포함했다.
| 서비스 | 실제로 확인한 동선 |
|---|---|
| 웹툰 | 요약 목록 → 대사 검색 → 회차 → 등장인물 |
| 블로그 | 검색 → 글 상세 → 아카이브 |
| 셀럽인사이트 | 이름 검색 → 프로필 → 인사이트 → 검색어 해석 |
읽기 전용 배너가 보이는지, 응답이 의도한 복구본에서 나오는지, 관리자·쓰기 요청이 차단되는지도 함께 확인했다.
여기에는 프론트엔드 관점에서 익숙한 함정이 있다.
서버가 HTML을 내려줬다고 버튼의 이벤트 핸들러까지 준비됐다는 뜻은 아니다. 첫 화면이 보인다고 검색 결과에서 상세 화면으로 이동하는 흐름까지 정상이라는 뜻도 아니다.
화면이 나타나는 것과 사용자의 작업이 끝나는 것은 서로 다른 검증 대상이다.
이번에는 단위 테스트 24개와 공개 브라우저 검사 15개를 통과했다.
물론 이 숫자가 모든 장애를 막아준다는 뜻은 아니다. 다만 “프로세스가 떠 있다”보다 구체적으로 무엇을 확인했는지 설명할 수 있게 됐다.
6. 자동화의 핵심은 전환 조건이었다
수동 복구가 끝나고 나니 다음 질문이 남았다.
“다음에도 내가 이 과정을 처음부터 반복해야 하나?”
그래서 검증된 대기 복구본을 주기적으로 준비하고, 장애 조건이 충족되면 공개 경로를 전환하도록 구성했다.
| 작업 | 실행 시점 |
|---|---|
| 새 백업 탐색과 복구본 갱신 | 매시간 |
| 일일 백업 결과를 반영한 갱신 시도 | 백업 작업 종료 후 |
| B 상태와 공개 응답 확인 | 매분 |
| 읽기 전용 경로 전환 | 두 장애 신호가 함께 5분간 지속될 때 |
여기서 중요한 부분은 마지막 줄이다.
공개 사이트가 한 번 응답하지 않았다고 바로 전환하지 않는다. B의 상태를 확인하는 명령이 실패했다는 이유만으로도 전환하지 않는다.
신선한 B 장애 관찰이나 B의 SSH 연결 불가와 함께, 공개 응답 실패가 지속되는지를 판단한다. SSH는 연결되는데 관찰 명령만 실패했다면 상태가 불확실한 것으로 취급한다.
오작동하는 자동화는 사람이 처리해야 할 장애를 하나 더 만들 수 있다. 자동 전환은 실행 속도보다 판단 근거가 먼저라고 생각했다.
백업 신선도에도 기준을 뒀다.
새 대기본 준비와 최초 공개 전환에는 촬영 시점이 48시간 이내인 백업을 요구한다. 다만 이미 공개 중인 복구본이 48시간을 넘었다고 사이트를 다시 내려버리지는 않는다.
오래된 데이터라는 한계를 알리면서 읽기 서비스를 유지하는 편이 이번 서비스에는 맞는 선택이었다.
7. 전환 중간에 멈추는 상황도 남겨두지 않았다
공개 경로 전환은 한 줄짜리 작업처럼 보이지만 실제로는 여러 단계다.
설정을 바꾸고, 터널을 재시작하고, DNS 상태를 확인하고, 공개 응답이 새 복구본에서 나오는지 검증해야 한다.
이 중간에 프로세스가 종료되면 어떻게 될까?
설정은 바뀌었는데 상태 기록은 이전 단계일 수 있다. 반대로 상태 기록에는 다음 단계라고 적혀 있지만, 재시작 후 실제 설정이 예상과 다를 수도 있다.
그래서 전환 작업의 진행 상태와 변경 전후 설정 해시를 기록했다. 다시 실행되면 실제 상태를 확인하면서 진행 중이던 전환을 이어가도록 했다.
이미 공개 중인 복구본이나 진행 중인 전환이 참조하는 복구본은 정리 대상에서 제외했다. 사용하지 않는 프로세스를 중지하더라도 복원 데이터와 증빙은 보존했다.
이번에는 역할을 분리했던 기존 구조가 도움이 됐다. 새로운 플랫폼으로 전부 옮기기보다, 기존 systemd와 터널 구성 위에 검증과 재개 절차를 추가했다.
장애가 난 날은 인프라를 전면 개편하기에 좋은 날이 아니었던 것 같다. 적어도 내 집중력에는 그랬다.
8. 자동 복귀는 넣지 않았다
B가 다시 응답하면 자동으로 원래 서버로 돌아가게 만들 수도 있다.
하지만 이번에는 그렇게 하지 않았다.
서버가 켜졌다는 사실만으로 앱과 DB가 정상이라고 판단할 수 없기 때문이다. 최신 데이터, 스키마, 서비스 상태와 실제 기능을 확인해야 한다.
자동 복귀까지 넣으면 자동화가 더 완성돼 보일 수 있다. 다만 확인해야 할 조건을 생략하면서 완성도를 높였다고 말하고 싶지는 않았다.
그래서 B 복귀는 검증을 거친 수동 절차로 남겼다.
이번 작업의 검증 범위도 구분해 둘 필요가 있다.
실제로 수행한 공개 전환은 이미 A에서 제공하던 임시 복구 서비스를, 새로 검증한 자동화 관리 복구본으로 교체한 작업이었다. B를 정상 상태에서 강제로 끊고 장애 감지부터 B→A 전환까지 전 과정을 재현한 시험은 아니었다.
B의 물리적 장애 원인과 재부팅 후 동작도 확인하지 못했다.
현재 복구본은 10월 13일에 확보한 백업을 기준으로 한다. B에 연결할 수 없는 동안에는 새 B 백업도 만들 수 없다. 새로운 복구본 준비에는 A와 Windows 저장소, 관련 네트워크가 필요하다.
이 정도는 완료 기록에 함께 남겨야 한다고 생각한다. 확인하지 못한 부분을 “일단 돌아가니까”로 덮으면 미래의 내가 그 문장을 믿는다. 꽤 위험한 독자다.
9. 결론: 복구 가능성을 확인하는 일
이번 작업을 하면서 운영에서 던지는 질문이 조금 더 구체적으로 바뀌었다.
“백업이 있나?” 다음에는 “그 백업을 실제로 복원해 봤나?”가 있었다.
“앱이 실행되나?” 다음에는 “사용자가 검색하고 상세 내용을 읽을 수 있나?”가 있었다.
“자동 전환이 되나?” 다음에는 “중간에 멈추면 어디서 다시 시작하나?”가 있었다.
주니어와 시니어의 차이를 단순히 연차로 나누기는 어렵지만, 이런 질문을 얼마나 앞당겨 던지는지는 경험과 관련이 있다고 생각한다. 복구 명령을 아는 것도 중요하다. 그 명령을 실행한 뒤 무엇을 확인해야 하는지 아는 일도 중요하다.
이번에 만든 것은 A의 검증된 백업으로 세 사이트를 읽을 수 있게 유지하는 장치다. 원래 운영 서버의 전원이나 네트워크 문제까지 해결한 것은 아니다.
그래도 다음 장애에서는 백업 파일을 찾는 일부터 다시 시작하지 않아도 된다. 어떤 보호본이 검증됐고, 어떤 조건에서 전환하며, 무엇을 아직 확인하지 못했는지 기록으로 남아 있다.
백업을 보관하는 데서 한 걸음 더 나아가, 그 백업으로 서비스를 제공할 수 있음을 확인했다.
이번 작업에서 가장 의미 있었던 변화는 그 부분이라고 생각한다.
댓글
첫 번째 댓글을 남겨보세요.