← 기술 블로그

Railway→AWS 프리티어 마이그레이션 — 비용 $0으로 프로덕션 전환

Phase: 8 — 운영 최적화 시리즈: 운영 최적화

이 글을 읽으면 알 수 있는 것


문제: Railway 무료 크레딧 만료 D-7

Railway는 초기에 $5 무료 크레딧을 줍니다. Django 앱과 PostgreSQL을 올리기에 충분했고, 배포는 Git push 한 번이면 끝나는 편리한 환경이었습니다.

그런데 크레딧 잔액이 $0.6이 되었습니다. 7일이면 바닥. 이후에는 월 $5~10이 청구될 예정이었습니다.

서비스를 계속 운영하려면 두 가지 선택지가 있었습니다.

선택지월 비용장단점
Railway 유료 전환$5~10편리하지만, 솔로 프로젝트에 매월 과금
AWS 프리티어 전환$012개월 무료, 대신 직접 구축

$0을 선택했습니다.


아키텍처 3안 비교

AWS 프리티어 안에서 Django + PostgreSQL을 돌릴 수 있는 조합을 세 가지 검토했습니다.

구성프리티어 적용월 예상 비용복잡도
EC2(t3.micro) + RDS(db.t3.micro)EC2 750h + RDS 750h 무료$0중간
ECS Fargate + RDSFargate는 프리티어 없음$10~20높음
Lightsail($3.5) + RDSLightsail 3개월만 무료4개월째부터 $3.5낮음

EC2 + RDS를 선택한 이유:

Terraform으로 IaC를 구성했습니다. 인프라를 코드로 관리하면, 나중에 똑같은 환경을 재현하거나 폐기할 때 실수가 줄어듭니다.

# EC2 인스턴스
resource "aws_instance" "app" {
  ami           = "ami-0c55b159cbfafe1f0"  # Amazon Linux 2023
  instance_type = "t3.micro"
  key_name      = aws_key_pair.deployer.key_name

  vpc_security_group_ids = [aws_security_group.app.id]

  tags = {
    Name = "checkeat-app"
  }
}

# RDS PostgreSQL
resource "aws_db_instance" "db" {
  identifier     = "checkeat-db"
  engine         = "postgres"
  engine_version = "16.4"
  instance_class = "db.t3.micro"
  allocated_storage = 20

  db_name  = "checkeat"
  username = var.db_username
  password = var.db_password

  skip_final_snapshot = true
}

PG17→PG16 마이그레이션: 버전 불일치 삽질

Railway의 PostgreSQL은 17이었고, AWS RDS 프리티어가 지원하는 최신 버전은 16이었습니다.

“마이너 버전 차이니까 그냥 되겠지”는 틀렸습니다.

첫 번째 벽: pg_dump 버전 불일치

$ pg_dump --version
pg_dump (PostgreSQL) 16.4

$ pg_dump -h railway-host -U postgres -d checkeat -Fc -f backup.dump
pg_dump: error: server version: 17.0; pg_dump version: 16.4
pg_dump: error: aborting because of server version mismatch

pg_dump는 자기보다 높은 버전의 서버를 덤프할 수 없습니다. Railway의 PG17 서버에서 데이터를 꺼내려면, PG17 버전의 pg_dump가 필요했습니다.

해결: PG17 클라이언트 도구만 별도 설치.

# PostgreSQL 17 클라이언트 도구 설치 (Ubuntu/Debian)
sudo apt-get install postgresql-client-17

# PG17 pg_dump로 Railway에서 덤프
/usr/lib/postgresql/17/bin/pg_dump \
  -h railway-host -U postgres -d checkeat \
  -Fc -f backup.dump

두 번째 벽: transaction_timeout 호환성

PG17 덤프를 PG16에 복원하니 에러가 쏟아졌습니다.

ERROR: unrecognized configuration parameter "transaction_timeout"

transaction_timeout은 PG17에서 새로 추가된 파라미터입니다. pg_dump가 서버 설정까지 함께 백업했고, PG16은 이 파라미터를 모릅니다.

해결: 덤프 파일에서 해당 설정을 제거하고 복원.

# 커스텀 포맷(-Fc)은 바이너리라 직접 편집 불가
# 플레인텍스트로 변환 → 제거 → 복원
pg_restore -f backup.sql backup.dump
sed -i '/transaction_timeout/d' backup.sql
psql -h rds-endpoint -U postgres -d checkeat -f backup.sql

이 두 단계를 알아내는 데 반나절이 걸렸습니다. “버전 하나 차이”의 실제 비용입니다.


데이터 검증: 17테이블 row count 100% 일치

5.2GB 원본 데이터를 pg_dump 커스텀 포맷(-Fc)으로 압축하니 922MB. 압축률 82%.

복원 후 가장 중요한 것은 데이터가 빠짐없이 옮겨졌는가입니다. 17개 테이블 각각에서 Railway와 RDS의 row count를 비교했습니다.

-- 각 테이블별 row count 확인
SELECT 
  schemaname,
  relname AS table_name,
  n_live_tup AS row_count
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC;
테이블Railway (PG17)RDS (PG16)일치
product1,604,2831,604,283O
raw_material892,451892,451O
nutrition743,219743,219O
… (14개 더)O
합계약 380만 건약 380만 건100%

17개 테이블 전부 일치. 이 검증을 건너뛰었다면, 나중에 “왜 이 데이터가 없지?”를 디버깅하느라 더 큰 시간을 썼을 것입니다.


DNS와 SSL: Route53 없이 $0으로

AWS Route53은 호스팅 영역 하나당 월 $0.5입니다. 작아 보이지만 프리티어가 아닙니다. 연간 $6.

가비아 DNS를 사용했습니다. 도메인을 가비아에서 구매했기 때문에, DNS 관리도 가비아에서 하면 추가 비용 $0.

# 가비아 DNS 설정
A    checkeat.com       → EC2 Elastic IP
A    api.checkeat.com   → EC2 Elastic IP

SSL은 Let’s Encrypt + certbot. 90일마다 자동 갱신.

# certbot 설치 및 인증서 발급
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d checkeat.com -d api.checkeat.com

# 자동 갱신 cron (매월 1일)
0 3 1 * * certbot renew --quiet

Route53($0.5/월) + ACM 대신 가비아 DNS(무료) + Let’s Encrypt(무료). 기능은 동일하고 비용만 다릅니다.


ETL cron 이전: PowerShell→Bash 변환

기존 ETL 스케줄러는 Windows Task Scheduler + PowerShell 스크립트였습니다. EC2는 Linux이므로 cron + Bash로 전환해야 했습니다.

Django 앱은 Docker 컨테이너 안에서 돌리고 있었기 때문에, ETL 커맨드 실행도 docker exec 방식으로 변경했습니다.

#!/bin/bash
# /home/ec2-user/scripts/etl_daily.sh

LOG_DIR="/home/ec2-user/logs"
DATE=$(date +%Y%m%d)

echo "[$(date)] ETL start" >> "$LOG_DIR/etl_$DATE.log"

# Docker 컨테이너 안에서 Django management command 실행
docker exec checkeat-app \
  python manage.py import_i1250 --resume \
  >> "$LOG_DIR/etl_$DATE.log" 2>&1

docker exec checkeat-app \
  python manage.py import_c002 --resume \
  >> "$LOG_DIR/etl_$DATE.log" 2>&1

echo "[$(date)] ETL end" >> "$LOG_DIR/etl_$DATE.log"
# crontab -e
# 매일 새벽 3시 ETL 실행
0 3 * * * /home/ec2-user/scripts/etl_daily.sh

PowerShell에서 Bash로의 변환 자체는 단순했지만, Docker exec 패턴으로 바꾸면서 로그 수집 방식을 통일할 수 있었습니다.


Vercel 배포: GitHub Org→개인 이전

프론트엔드는 Vercel에 배포하고 있었는데, GitHub Organization 레포에 연결되어 있었습니다. Vercel Hobby 플랜(무료)은 개인 계정만 지원하고, Organization 연결 시 Pro 플랜($20/월)이 필요합니다.

해결: 레포를 Organization에서 개인 계정으로 이전(Transfer).

GitHub Settings → Transfer ownership → 개인 계정
Vercel → Git 연동 재설정 → 개인 레포 연결

이것만으로 Vercel 비용 $0 유지. 솔로 프로젝트에서 Organization을 쓸 이유가 없었다는 교훈.


소소한 삽질들

SSH 키 자동 생성

Terraform으로 EC2를 만들 때, SSH 키 페어를 Terraform이 생성하도록 설정하면 .pem 파일이 로컬에 자동 저장됩니다.

resource "tls_private_key" "deployer" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

resource "aws_key_pair" "deployer" {
  key_name   = "checkeat-deployer"
  public_key = tls_private_key.deployer.public_key_openssh
}

# 로컬에 .pem 파일 저장
resource "local_file" "private_key" {
  content         = tls_private_key.deployer.private_key_pem
  filename        = "${path.module}/checkeat-deployer.pem"
  file_permission = "0400"
}

AWS 콘솔에서 수동으로 키를 만들고 다운로드하는 것보다, Terraform으로 관리하면 키 분실 위험이 줄고 재생성도 terraform apply 한 번이면 됩니다.

Security Group 한글 에러

Security Group 설명(description)에 한글을 넣으면 AWS API가 거부합니다.

# 에러 발생
resource "aws_security_group" "app" {
  description = "체크잇 앱 서버 보안그룹"  # InvalidParameterValue
}

# 영문으로 변경
resource "aws_security_group" "app" {
  description = "CheckEat app server security group"  # OK
}

에러 메시지도 불친절해서, 처음에는 원인을 찾는 데 시간이 걸렸습니다. AWS 리소스의 description 필드는 영문만 사용하는 것이 안전합니다.


프리티어 만료 대비: RDS→Docker PostgreSQL 전환 계획

2025년 시점에서 AWS 프리티어 기간이 12개월에서 6개월로 변경되었다는 점을 알게 되었습니다. RDS 프리티어가 끝나면 db.t3.micro 비용은 월 $15~20.

프리티어 만료 후 계획:

현재: EC2(프리티어) + RDS(프리티어)
  ↓ RDS 프리티어 만료 후
전환: EC2 + Docker PostgreSQL (EC2 안에서 실행)

RDS를 폐기하고, EC2 안에서 Docker로 PostgreSQL을 돌리면 추가 비용 $0. 단, t3.micro의 메모리(1GB)에서 Django 앱 + PostgreSQL을 함께 돌려야 하므로 메모리 튜닝이 필요합니다.

# docker-compose.yml (전환 후 구성)
services:
  app:
    image: checkeat-app
    ports:
      - "8000:8000"
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: checkeat
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    # 메모리 제한 (t3.micro 1GB 중 400MB 할당)
    deploy:
      resources:
        limits:
          memory: 400M

volumes:
  pgdata:

이 전환은 pg_dump/pg_restore로 데이터를 옮기면 됩니다. 이번에 PG17→PG16 마이그레이션을 경험했기 때문에, 같은 PG16→PG16 전환은 훨씬 수월할 것입니다.


최종 결과

항목Railway (이전)AWS (이후)
월 비용$5~10 (크레딧 소진 후)$0 (프리티어)
DBPostgreSQL 17 (관리형)PostgreSQL 16 (RDS 관리형)
배포Git push (자동)Docker + 수동 배포
DNSRailway 제공가비아 (무료)
SSLRailway 제공Let’s Encrypt (무료)
ETLWindows Task SchedulerLinux cron + Docker exec
IaC없음Terraform
마이그레이션 항목수치
데이터 원본 크기5.2GB
pg_dump 압축 후922MB (압축률 82%)
테이블 수17개
총 row count약 380만 건
검증 결과17/17 테이블 100% 일치
소요 시간약 3일 (삽질 포함)

교훈


이전 글: Django + PostgreSQL 160만 건 검색, 4.5초→0.05초까지