1. 마이그레이션 배경
프리 티어 기간이 곧 끝나기 때문에 지금 운영하는 이 개인 블로그 서비스의 백엔드를 기존 AWS 계정에서 새로운 AWS 계정으로 이전해야 했다. 프론트엔드는 Next.js로 개발해 Vercel에 배포하고 있고, 백엔드는 EC2에서 Docker Compose로 운영하고 있었다.
백엔드 EC2 구성은 다음과 같았다.
- Nginx
- FastAPI
- PostgreSQL
- Redis
- RQ worker
- Amazon S3
- GitHub Actions self-hosted runner
처음에는 새 계정에 EC2를 생성하고 Git 저장소를 clone한 뒤 Docker Compose를 실행하면 될 것으로 단순하게만 생각했다.
하지만 실제 이전 범위를 확인해보니 EC2만 옮겨서는 서비스를 유지할 수 없었다. PostgreSQL 데이터, S3 객체, 게시글에 저장된 이미지 URL, HTTPS 인증서, DNS, CI/CD runner가 모두 기존 AWS 계정 및 EC2와 연결되어 있었다.
따라서 이번 작업의 목표를 다음과 같이 정했다.
기존 서비스를 유지한 상태에서 새 인프라를 병렬로 구성하고, 충분한 검증이 끝난 후 DNS를 전환한다.
엄밀히 말하면 완전한 무중단 마이그레이션은 아니다. 최종 전환 과정에서 블로그 글쓰기 작업을 잠시 중단했고 DNS 캐시 만료 시간도 필요했다.
대신 공개 게시글 조회는 최대한 유지하면서 데이터 정합성과 롤백 가능성을 확보하는 방향으로 계획했다.
2. 기존 서비스 구조
기존 서비스는 Vercel에 배포된 프론트엔드와 AWS EC2에 배포된 백엔드로 구성되어 있었다.
| 영역 | 구성 |
|---|---|
| 프론트엔드 | Next.js, Vercel |
| 백엔드 | FastAPI, EC2 |
| 리버스 프록시 | Nginx |
| 데이터베이스 | PostgreSQL 컨테이너 |
| 캐시 및 작업 큐 | Redis |
| 비동기 처리 | RQ worker |
| 파일 저장소 | Amazon S3 |
| HTTPS | Let’s Encrypt, Certbot |
| CI/CD | GitHub Actions self-hosted runner |
| 프론트 도메인 | www.hahbr88.site |
| API 도메인 | api.hahbr88.site |
사용자가 블로그에 접속하면 Vercel이 페이지를 제공하고, 데이터가 필요한 경우 기존 EC2의 API를 호출했다.
이미지 업로드는 FastAPI가 처리했으며, 원본 이미지와 worker가 생성한 썸네일은 기존 AWS 계정의 S3에 저장했다.
마이그레이션 이전 백엔드 아키텍처. Vercel에서 기존 EC2의 API를 호출하고, FastAPI·PostgreSQL·Redis·RQ worker가 Docker Compose 기반으로 운영됐다. 게시글 이미지는 기존 S3 객체 URL을 통해 직접 제공됐다.
3. 단순 EC2 복제로 끝나지 않는 이유
EC2 마이그레이션에서 가장 먼저 확인한 것은 애플리케이션 외부에 존재하는 상태 데이터였다.
PostgreSQL 데이터
PostgreSQL은 EC2의 Docker volume을 사용하고 있었다.
새 EC2에서 동일한 Compose 파일을 실행하면 빈 데이터베이스가 생성된다. 기존 게시글과 업로드 이력을 유지하려면 별도의 dump와 복원 과정이 필요했다.
S3 객체
게시글 이미지와 썸네일은 기존 AWS 계정의 S3에 저장되어 있었다.
새 애플리케이션이 정상적으로 실행되더라도 기존 S3를 계속 참조하면 AWS 계정 이전이 완료됐다고 보기 어렵다. S3 객체를 새 계정으로 복사하고, 버킷 정책과 IAM 권한을 새 계정에서 다시 구성해야 했다.
이미지 절대 URL
데이터베이스의 업로드 테이블과 게시글 본문에는 기존 S3 주소가 절대 URL 형태로 저장되어 있었다.
따라서 새 S3 버킷으로 객체를 복사하는 것만으로는 충분하지 않았다. 버킷 주소가 달라지면 기존 게시글의 이미지 링크가 깨질 수 있었다.
DNS와 HTTPS
api.hahbr88.site는 기존 EC2의 퍼블릭 IP를 가리키고 있었다.
새 EC2를 준비해도 DNS를 변경하기 전까지 사용자의 요청은 기존 EC2로 전달된다. 반대로 준비가 끝나기 전에 DNS를 변경하면 장애로 이어질 수 있다.
HTTPS 인증서 역시 기존 EC2의 Nginx와 Certbot에 연결되어 있었다.
GitHub Actions runner
운영 배포는 기존 EC2의 self-hosted runner에서 실행되고 있었다.
새 EC2에 runner를 추가하더라도 기존 runner와 동일한 label을 사용하면 어느 서버에서 배포가 실행될지 보장하기 어렵다. CI/CD 전환도 별도 작업으로 다뤄야 했다.
4. 주요 위험과 대응 방향
마이그레이션을 시작하기 전에 예상 가능한 실패 지점을 먼저 정리했다.
| 위험 | 발생 가능한 문제 | 대응 방향 |
|---|---|---|
| DB 시점 불일치 | 기존·신규 DB의 게시글 차이 | 최종 dump 시점부터 관리자 쓰기 중단 |
| S3 객체 누락 | DB에는 URL이 있지만 실제 객체가 없음 | 객체 수와 전체 용량 비교 |
| 이미지 URL 변경 | 기존 게시글 이미지가 깨짐 | 고정 이미지 도메인 도입 |
| DNS 전파 지연 | 일부 요청이 기존 EC2로 전달 | 기존 서버 유지 및 TTL 고려 |
| HTTPS 오류 | DNS 전환 후 인증서 오류 | 전환 전 새 EC2의 TLS 검증 |
| runner 충돌 | 기존 서버로 잘못 배포 | 신규 runner 전용 label 사용 |
| 자격증명 노출 | Access Key 또는 .env 유출 | EC2 IAM Role과 .dockerignore를 통한 환경 파일 제외 설정 |
| 롤백 실패 | 장애 발생 후 기존 환경 복귀 불가 | 기존 EC2와 S3를 일정 기간 유지 |
이 표를 기준으로 실제 작업 순서를 결정했다.
5. 목표 아키텍처
마이그레이션 이후에는 프론트엔드, API, 이미지 제공 경로를 명확하게 분리하기로 했다.
| 도메인 | 역할 | 대상 |
|---|---|---|
www.hahbr88.site | 프론트엔드 | Vercel |
api.hahbr88.site | 백엔드 API | 새 AWS 계정의 EC2 |
assets.hahbr88.site | 이미지 제공 | CloudFront |
| S3 | 실제 이미지 저장 | CloudFront가 접근하는 비공개 S3 origin |
기존에는 데이터베이스에 S3 버킷 주소가 직접 저장됐다.
새 구조에서는 assets.hahbr88.site를 이미지의 고정 주소로 사용한다. 실제 S3 버킷이나 CloudFront 배포가 변경되더라도 데이터베이스의 이미지 URL은 유지할 수 있다.
또한 FastAPI와 worker는 Access Key 대신 EC2 IAM Role을 사용해 새 S3에 접근하도록 계획했다.
6. 마이그레이션 핵심 원칙
기존 서비스는 검증이 끝날 때까지 유지한다
새 환경이 완성되기 전에 기존 EC2나 S3를 변경하지 않는다.
새 환경에서 문제가 발견되더라도 사용자는 계속 기존 서비스를 이용할 수 있어야 한다.
먼저 복제하고 마지막에 전환한다
새 EC2, PostgreSQL, Redis, S3, CloudFront, Nginx를 먼저 준비한다.
새 환경을 독립적으로 검증한 뒤 마지막에 API DNS를 변경한다.
데이터 전송 결과를 수치로 검증한다
명령이 성공했다는 사실만으로 이전 완료를 판단하지 않는다.
- DB dump 해시
- 게시글 개수
- 업로드 레코드 개수
- Alembic migration version
- S3 객체 수
- S3 전체 용량
이 값들을 기준으로 기존 환경과 새 환경을 비교한다.
전환 지점을 최소화한다
DNS 변경 시점에는 새 환경이 이미 운영 가능한 상태여야 한다.
최종 전환 작업은 최대한 api.hahbr88.site의 A 레코드 변경에 집중하도록 구성한다.
롤백 경로를 먼저 정한다
전환 후 문제가 발생하면 DNS를 기존 EC2 IP로 복구할 수 있어야 한다.
이를 위해 기존 EC2와 S3는 새 환경이 안정화될 때까지 삭제하지 않는다.
7. 전체 마이그레이션 계획
사전 조사
- 기존 EC2의 서비스 구성 확인
- Docker Compose 및 포트 노출 상태 확인
- 환경변수와 S3 설정 확인
- Nginx와 HTTPS 구성 확인
- GitHub Actions runner 구성 확인
- 데이터 백업 및 롤백 범위 결정
신규 환경 준비
- 새 AWS 계정에 EC2 생성
- Docker와 Docker Compose 설치
- EC2 IAM Role 연결
- 새 S3 버킷 생성
- 백엔드 migration 브랜치 준비
데이터 이전
- PostgreSQL dump 생성
- dump 파일 해시 검증
- 새 PostgreSQL에 데이터 복원
- 게시글·업로드·Alembic migration version 확인
- S3 객체 이전
- S3 객체 수와 전체 용량 비교
이미지 제공 구조 변경
- CloudFront 배포 생성
- 비공개 S3와 CloudFront 연결
- 이미지 전용 도메인 구성
- 기존 이미지 URL 변경
- Vercel에서 새 이미지 도메인 허용
전환 및 검증
- 새 EC2의 API와 worker 검증
- Nginx와 HTTPS 사전 검증
- 관리자 쓰기 작업 중단
- API DNS 변경
- 공개 API와 블로그 화면 확인
- 인증서 자동 갱신 확인
CI/CD 이전
- 새 self-hosted runner 등록
- 기존 runner와 label 분리
- 배포 workflow 대상 변경
- main 병합 후 자동 배포 확인
- 기존 runner 제거
안정화 및 정리
- 새 환경의 로그와 health check 확인
- 관리자 쓰기 작업 재개
- 기존 EC2와 S3를 롤백 기간 동안 유지
- 안정화 기간 이후 기존 리소스 삭제
8. 성공 기준
이번 마이그레이션의 완료 조건은 단순히 새 EC2에서 컨테이너가 실행되는 것만이 아니다.
| 영역 | 완료 기준 |
|---|---|
| PostgreSQL | 게시글·업로드 개수 및 Alembic migration version 일치 |
| S3 | 객체 수와 전체 용량 일치 |
| 이미지 | 고정 이미지 도메인에서 정상 응답 |
| API | 내부·외부 health check 성공 |
| HTTPS | 인증서 발급 및 자동 갱신 검증 |
| 네트워크 | PostgreSQL과 Redis의 외부 포트 제거 |
| 프론트엔드 | 새 이미지 주소를 정상적으로 처리 |
| CI/CD | 새 EC2 runner에서 운영 배포 성공 |
| 보안 | Docker 이미지에 .env가 포함되지 않음 |
| 롤백 | 기존 EC2와 S3를 통한 복구 가능 |
이 조건을 모두 확인한 이후에만 마이그레이션이 완료된 것으로 판단한다.
9. 롤백 계획
DNS 변경 전 문제가 발생한 경우
DNS를 변경하지 않고 새 EC2에서 문제를 해결한다.
기존 서비스에는 영향을 주지 않으므로 별도의 복구 작업이 필요하지 않다.
DNS 변경 직후 문제가 발생한 경우
api.hahbr88.site의 A 레코드를 기존 EC2 IP로 복구한다.
기존 EC2와 S3를 그대로 유지하고 있으므로 트래픽을 다시 기존 환경으로 돌릴 수 있다.
새 환경에서 데이터가 변경된 이후 문제가 발생한 경우
관리자 쓰기 작업을 다시 중단하고 새 데이터베이스의 변경 내용을 먼저 확인한다.
필요한 데이터를 기존 데이터베이스에 반영한 뒤 DNS를 복구해야 한다. 그렇지 않으면 새 환경에서 작성한 게시글이나 업로드 정보가 유실될 수 있다.
이 위험을 줄이기 위해 DNS 전환과 핵심 기능 검증이 끝날 때까지 관리자 쓰기 동결을 유지한다.
10. 마무리
이번 작업을 계획하면서 EC2 마이그레이션은 단순히 서버를 새로 만들고 애플리케이션을 실행하는 작업이 아니라는 점을 확인했다.
애플리케이션 외에도 다음 요소를 함께 고려해야 했다.
- 상태 데이터
- 파일 저장소
- 데이터에 저장된 외부 URL
- IAM 권한
- HTTPS 인증서
- DNS
- CI/CD runner
- 롤백 경로
특히 기존 S3 주소가 게시글 데이터에 직접 저장된 구조는 인프라 변경 비용을 크게 만들었다.
이를 해결하기 위해 고정 이미지 도메인을 도입했고, 실제 저장소와 데이터에 기록되는 URL을 분리했다.
또한 새 환경을 먼저 구성하고 충분히 검증한 뒤 DNS를 마지막에 변경함으로써 실제 서비스에 미치는 영향을 줄일 수 있었다.
마이그레이션에서 중요한 것은 빠르게 옮기는 것이 아니라, 전환 전 검증과 실패했을 때 되돌아갈 수 있는 경로를 확보하는 것이다.
다음 글에서는 PostgreSQL dump 생성과 검증, 새 데이터베이스 복원, S3 객체 이전, IAM Role 구성, 기존 이미지 URL 변경 과정을 실제 작업 순서와 결과를 중심으로 정리할 예정이다.