SPACE LOGBOOK

OCEAN LOGBOOK

서버 마이그레이션 작업..

2026.08.08

서버 마이그레이션 작업..

2026.08.08

🚀 서버 이사, 완료. — xx.xxx.xx.x9 → xx.xxx.xxx.x8 마이그레이션 기록

#서버운영 #AWS-Lightsail #새솔CRM #양도통 #nginx #pm2 #타임존버그

2026년 8월 9일, 새벽 2시부터 아침까지. 세무사무소 CRM과 가족 웹앱이 돌아가던 라이트세일 인스턴스 하나를 통째로 다른 인스턴스로 옮겼다. 계획 세우고, 옮기고, 터진 걸 고치고, 또 하나 더 터진 걸 고치고… 결과적으로는 무중단에 가깝게 끝났지만 과정은 절대 깔끔하지만은 않았다. “완벽한 이전”을 목표로 삼았던 기록을 남긴다.

왜 옮겼나

발단은 단순했다. 새솔CRM이 원래 돌아가던 xx.xxx.xx.x9 인스턴스는 RAM이 총 416MB뿐인 작은 사양이었다. 확인해보니 이미 스왑을 289MB나 쓰고 있는, 말 그대로 “빠듯한” 상태였다. 반면 워드프레스(********.com)와 최근 새로 배포한 양도소득세 계산 웹앱(양도통)이 돌아가는 xx.xxx.xxx.x8 인스턴스는 RAM 1.9GB에 여유가 1.2GB나 남아 있었다.

xx.xxx.xx.x9 (구서버)

  • RAM 416MB, 여유 116MB
  • 스왑 289MB/1GB 이미 사용 중
  • 디스크 20GB 중 48% 사용
  • 새솔CRM · family-connect · gmail-poller

xx.xxx.xxx.x8 (신서버)

  • RAM 1.9GB, 여유 1.2GB
  • 스왑 210MB/2GB 경미하게 사용
  • 디스크 59GB 중 14% 사용
  • 워드프레스(********.com) · 양도통

자원 여유를 실측 데이터로 먼저 확인하고 나서야 결정했다. “여유 있어 보인다”는 감이 아니라, RAM/디스크/CPU 로드를 직접 찍어보고 비교한 다음에 움직인 것 — 이번 작업에서 가장 잘한 선택 중 하나였다고 생각한다.

사전 조사: 계획이 반이다

바로 옮기지 않고 먼저 체크리스트를 만들었다. 특히 신경 쓴 부분은 세 가지였다.

1. 계정이 다르다 — ubuntu vs bitnami

구서버는 ubuntu 계정, 신서버는 bitnami 계정을 쓰고 있었다. 계정명 자체는 문제가 안 되지만, 코드와 스크립트 곳곳에 /home/ubuntu/... 절대경로가 하드코딩돼 있을 거라는 예상이 들었고, 실제로 grep을 돌려보니 6곳에서 발견됐다.

위치내용조치
.env의 SECRETARY_ROOT_DIRWorkDocs 경로 하드코딩새 경로로 수정 필수
백업 스크립트 3종백업 경로 하드코딩새 경로로 치환 후 재배치
crontab 8줄스크립트 경로 하드코딩전부 치환 후 재등록
family ecosystem.config.cjscwd 경로새 경로로 수정
pm2 시작 경로엔트리포인트 경로새 경로 기준 재등록
도움말 화면 텍스트화면에 보이는 안내 문구기능 영향 없음, 선택 수정

2. 시스템 타임존이 다르다 — 나중에 진짜 문제가 됨

구서버는 Asia/Seoul(KST), 신서버는 UTC였다. 미리 감지는 했다. 코드를 훑어보니 알림/크론 로직 대부분이 timeZone: 'Asia/Seoul'을 명시적으로 지정하고 있어서 “시스템 타임존과 무관하게 정상 동작할 것”이라고 판단하고, 크론 시각만 UTC로 환산해서 등록하는 방식(UTC 유지)을 택했다. 이 판단이 나중에 틀렸다는 걸 실제 접수 테스트에서 알게 된다. (아래 사건 일지 참고)

3. Git 워크플로우 재현 가능성

새솔CRM은 “서버에서 직접 git commit & push” 방식으로 운영해왔다. 이걸 신서버에서도 그대로 살릴 수 있는지 미리 확인이 필요했다. (역시 나중에 진짜 이슈가 됐다 — 아래에서 다룬다.)

실행 타임라인

STEP 0 · 다운타임 없음 타겟 서버 준비 — 앱 디렉토리 생성, Node 버전 확인(양쪽 다 v20.20.2로 동일 → 네이티브 모듈 재빌드 호환성 문제 없음).
STEP 1 · 다운타임 없음 사전 데이터 1차 동기화 — node_modules, 오래된 백업 스냅샷 제외하고 tar+scp로 전송(saesol-crm-web ≈400MB대, family 102MB, WorkDocs 6MB). 타겟에서 npm install --omit=dev로 네이티브 모듈 재빌드, family는 소스에서 새로 npm run build — 옛 dist를 믿지 않고 최신 소스 기준으로 검증.
STEP 2 · 짧은 다운타임 시작 최종 커트오버 — 크론 비활성화 → DB(PRAGMA wal_checkpoint(TRUNCATE))로 WAL 플러시 → pm2 stop으로 구서버 앱 정지 → 최종 diff만 재동기화 → .env·ecosystem.config.cjs 이전(경로 수정, family의 방치돼 있던 SESSION_SECRET은 이 기회에 강한 값으로 교체) → 신서버에서 pm2 start.
STEP 3 nginx / SSL — 기존 ******** nginx 설정 파일 하나에 crm.********.com, family.********.com server block 추가. 인증서는 별도 발급이 아니라 기존 WebsiteSetupLECert에 SAN으로 확장(certbot --expand) — standalone 방식이라 갱신 때마다 nginx를 잠깐 멈췄다 올리는 구조.
STEP 4 DNS 전환 — Route53에서 두 서브도메인 A레코드를 신서버 IP로 변경, 전파 확인.
STEP 5 검증 — 헬스체크, 마스터 로그인, gmail-poller 로그, PM2 크래시 복구 테스트(kill -9 후 자동 재기동 + 텔레그램 알림 확인), 윈도우 트레이 에이전트 재확인.
STEP 6 크론 재설정 — 일 3회 DB 백업, WAL 체크포인트, 만료 토큰 정리, 캐비닛 정리, 주간 전체백업 + 텔레그램 알림까지 총 8개 잡을 UTC 환산 시각으로 재등록.

사건 일지: 옮기고 나서 터진 것들

계획대로 커트오버까지는 순조로웠다. 문제는 그 다음, “실제로 써보니까” 하나씩 튀어나왔다.

🔴 사건 1 — 실시간 알림이 계속 끊김

crm.********.com의 SSE(실시간 이벤트, /api/events) 연결이 1분마다 끊기고 재연결을 반복. 원인은 nginx 기본 proxy_read_timeout(60초) — 유휴 상태의 SSE 연결이 계속 타임아웃에 걸린 것.

✅ 조치

/api/events 전용 location을 추가해 타임아웃을 1시간으로 늘리고 버퍼링을 껐다. 이전과 무관하게 원래도 있었을 구조적 이슈였을 가능성이 높다.

🔴 사건 2 — sqlite3 CLI가 신서버에 아예 없었음

백업 스크립트와 만료 토큰 정리 크론이 내부적으로 sqlite3 명령을 호출하는데, 신서버엔 이 도구 자체가 설치돼 있지 않았다. 에러 없이 조용히 스킵되는 구조라 겉으로는 “정상”처럼 보였다 — 백업 파일 자체는 생성됐으니까.

✅ 조치

apt-get install sqlite3로 설치 후 재테스트, WAL 체크포인트 정상 동작 확인.

🔴 사건 3 — 진짜 문제: 접수 시각이 9시간씩 밀려서 찍힘

워드프레스에서 상담 예약을 테스트해봤더니, 텔레그램·CRM 알림은 정상적으로 왔는데 접수 일시가 실제 시각(KST 03:01)이 아니라 UTC 그대로(18:01, 즉 전날 오후) 찍혀 나왔다. 사전 조사 때 “코드가 timeZone: 'Asia/Seoul'을 명시하니 UTC 서버여도 괜찮을 것”이라 판단했던 게 틀렸다는 걸 실사용 테스트에서 처음 발견한 순간.

원인을 파고들어 보니, SQLite 스키마 기본값과 다수의 쿼리에서 datetime('now', 'localtime') 패턴을 코드베이스 전체 49군데에서 쓰고 있었다. 이건 timeZone 옵션과 별개로 순수하게 OS 시스템 시각을 그대로 따라가는 SQLite 함수라, 서버가 UTC면 UTC 그대로 찍히는 게 당연했다.

“코드 49군데를 하나씩 고치는 것보다, 서버 시간대 자체를 되돌리는 게 훨씬 안전하고 확실하다.” — 이 판단으로 방향을 틀었다.
✅ 조치

서버 시스템 타임존을 UTC → Asia/Seoul로 전환(timedatectl set-timezone). 크론도 UTC 환산값을 걷어내고 원래 KST 시각으로 되돌렸다. 4개 앱(새솔CRM·양도통·family·gmail-poller) 전부 재시작 후 datetime('now','localtime')이 실제 서버 시각과 정확히 일치하는지 재검증. 이후 재테스트에서 접수 시각이 실시간과 정확히 맞아떨어지는 걸 확인했다.

🔴 사건 4 — 알림톡 전송결과조회가 안 됨

Aligo(알림톡) API 호출 시 "인증되지 않는 서버 IP로부터의 호출입니다" 에러. Aligo가 발신 서버 IP를 화이트리스트로 관리하고 있었는데, 구서버 IP만 등록돼 있던 것.

✅ 조치

Aligo 관리자 페이지에서 등록 서버 IP를 신서버 IP로 교체. 즉시 정상화(code:0, "정상적으로 호출하였습니다").

🔴 사건 5 — .git 없이 tar로만 옮겨서, 서버에서 커밋·푸시가 안 됨

이전 방식 자체가 .git 폴더를 제외한 tar 복사였기 때문에, 신서버의 앱 디렉토리엔 애초에 git 저장소가 없었다. 예전처럼 “서버에서 직접 commit & push” 하던 워크플로우가 막힌 상태.

✅ 조치

각 앱(saesol-crm, family)마다 전용 SSH 배포키(write 권한)를 신서버에서 새로 생성해 GitHub 저장소에 등록. 기존 디렉토리는 .old로 백업해두고 GitHub에서 fresh clone → DB·업로드파일·.env 등 git이 추적하지 않는 실데이터만 골라서 복원(git 추적 파일은 건너뛰는 방식으로 안전하게). 실제 테스트 커밋으로 push까지 확인 후 정리.


최종 점검 결과

모든 이슈를 해결한 뒤 종합 점검을 돌렸다.

  • 4개 앱(새솔CRM·양도통·family·워드프레스) 전부 에러 로그 없음, 전 도메인 HTTPS 200
  • DB 무결성 체크(PRAGMA integrity_check) 전부 ok — 이전 과정에서 데이터 손상 없음 확인
  • SSL 인증서 유효기간 2026-11-06, 자동갱신 크론 정상
  • PM2 크래시 복구 테스트 통과(강제 종료 → 자동 재기동 → 텔레그램 알림)
  • 윈도우 트레이 에이전트(agent.py)는 IP가 아닌 도메인(crm.********.com)으로 통신하는 구조라, 서버 IP가 바뀌어도 코드 수정이나 재설정 없이 자동으로 새 서버에 연결됨 — 실제로 재설정 없이 정상 heartbeat 확인
  • UptimeRobot도 마찬가지로 도메인 기반 모니터링이라 별도 설정 변경 없이 자동 전환
  • 최종 자원 현황: RAM 여유 약 1GB, 디스크 여유 48GB — 4개 앱을 한 서버에 다 올리고도 넉넉

💰 덤으로 얻은 것 — 인스턴스 정리

이번 이전으로 xx.xxx.xx.x9(메모리 500MB / SSD 20GB) 인스턴스가 완전히 비었다. 이제 이 인스턴스는 삭제해도 되고, 앞으로는 메모리 2GB / SSD 60GB짜리 인스턴스 하나로 워드프레스 · 새솔CRM · family · 양도통 4개 서비스를 전부 감당하면 된다. 서버 대수가 둘에서 하나로 줄었으니 라이트세일 월 비용도 그만큼 절감되는 셈 — 성능상 이점뿐 아니라 유지비 절감까지 챙긴 마이그레이션이었다.

배운 것들

  1. 자원 여유는 감이 아니라 숫자로 확인하고 결정한다. RAM/디스크/로드를 실측해서 비교한 게 전체 계획의 출발점이었다.
  2. “코드가 타임존을 명시했으니 괜찮을 것”이라는 가정은 실사용 테스트로 반드시 검증해야 한다. SQL 함수 레벨(datetime('now','localtime'))에서 시스템 타임존에 의존하는 코드는 애플리케이션 레벨의 타임존 옵션과 완전히 별개로 동작한다.
  3. IP 기반 화이트리스트(Aligo)는 마이그레이션 체크리스트에 반드시 포함시켜야 한다. 서버를 옮기면 외부 서비스에 등록된 발신 IP도 같이 옮겨야 한다는 걸 당연하게 여기지 말 것.
  4. 도메인 기반으로 설계된 통합(PC 에이전트, 업타임 모니터링)은 서버 이전에 강하다. IP를 하드코딩하지 않고 도메인으로 통신하게 만든 과거의 설계 선택이 이번 이전을 훨씬 수월하게 만들어줬다.
  5. 겉보기엔 정상이어도 조용히 실패하고 있을 수 있다. sqlite3 CLI 미설치처럼, 스크립트가 에러 없이 그냥 스킵되는 실패 패턴은 발견하기 어렵다. 배포 후에는 반드시 “진짜로 동작하는지” 수동 실행 테스트를 해봐야 한다.
  6. 롤백 안전망은 끝까지 남겨둔다. 구서버는 인스턴스를 정지(stop)만 시켜뒀고, 신서버의 옛 디렉토리(.old)도 며칠은 그대로 보존하기로 했다. 삭제는 안정화 기간을 충분히 거친 뒤에.
✦ 마무리
하루 안에 서버 하나를 통째로 옮기고, 그 과정에서 타임존 버그·nginx 타임아웃·IP 화이트리스트·git 워크플로우까지 연쇄적으로 터진 걸 하나씩 잡아냈다. 딱 하루 만에 계획·실행·트러블슈팅·검증을 전부 끝냈다는 게 스스로도 좀 놀랍다. vibe coding으로 시작한 새솔CRM이 이제는 서버 인프라 수준의 작업까지 감당할 수 있게 됐다는 게 오늘의 가장 큰 소득이다. 다음 이전 작업(혹은 신규 인스턴스 구축)이 있다면, 이 기록이 그대로 체크리스트가 되어줄 것이다.

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

이 기록은 여기까지지만,
생각은 아직 이동 중이다