이 글을 읽으면 알 수 있는 것
- Railway 크레딧 만료 D-7에서 AWS 프리티어 전환까지의 전체 과정
- EC2+RDS vs ECS vs Lightsail — 프리티어 관점의 아키텍처 비교
- PG17→PG16 마이그레이션에서 pg_dump 버전 불일치 문제와 해결
- Route53 없이 가비아 DNS + Let’s Encrypt로 HTTPS 구성하기
- 프리티어 만료 후 RDS→Docker PostgreSQL 전환 계획
문제: Railway 무료 크레딧 만료 D-7
Railway는 초기에 $5 무료 크레딧을 줍니다. Django 앱과 PostgreSQL을 올리기에 충분했고, 배포는 Git push 한 번이면 끝나는 편리한 환경이었습니다.
그런데 크레딧 잔액이 $0.6이 되었습니다. 7일이면 바닥. 이후에는 월 $5~10이 청구될 예정이었습니다.
서비스를 계속 운영하려면 두 가지 선택지가 있었습니다.
| 선택지 | 월 비용 | 장단점 |
|---|---|---|
| Railway 유료 전환 | $5~10 | 편리하지만, 솔로 프로젝트에 매월 과금 |
| AWS 프리티어 전환 | $0 | 12개월 무료, 대신 직접 구축 |
$0을 선택했습니다.
아키텍처 3안 비교
AWS 프리티어 안에서 Django + PostgreSQL을 돌릴 수 있는 조합을 세 가지 검토했습니다.
| 구성 | 프리티어 적용 | 월 예상 비용 | 복잡도 |
|---|---|---|---|
| EC2(t3.micro) + RDS(db.t3.micro) | EC2 750h + RDS 750h 무료 | $0 | 중간 |
| ECS Fargate + RDS | Fargate는 프리티어 없음 | $10~20 | 높음 |
| Lightsail($3.5) + RDS | Lightsail 3개월만 무료 | 4개월째부터 $3.5 | 낮음 |
EC2 + RDS를 선택한 이유:
- EC2 t3.micro는 프리티어로 월 750시간 무료 (1대 24시간 가동 가능)
- RDS db.t3.micro도 월 750시간 + 20GB 스토리지 무료
- ECS Fargate는 프리티어가 없어서 탈락
- Lightsail은 3개월 이후 유료 전환이 확정적이라 탈락
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) | 일치 |
|---|---|---|---|
| product | 1,604,283 | 1,604,283 | O |
| raw_material | 892,451 | 892,451 | O |
| nutrition | 743,219 | 743,219 | O |
| … (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 (프리티어) |
| DB | PostgreSQL 17 (관리형) | PostgreSQL 16 (RDS 관리형) |
| 배포 | Git push (자동) | Docker + 수동 배포 |
| DNS | Railway 제공 | 가비아 (무료) |
| SSL | Railway 제공 | Let’s Encrypt (무료) |
| ETL | Windows Task Scheduler | Linux cron + Docker exec |
| IaC | 없음 | Terraform |
| 마이그레이션 항목 | 수치 |
|---|---|
| 데이터 원본 크기 | 5.2GB |
| pg_dump 압축 후 | 922MB (압축률 82%) |
| 테이블 수 | 17개 |
| 총 row count | 약 380만 건 |
| 검증 결과 | 17/17 테이블 100% 일치 |
| 소요 시간 | 약 3일 (삽질 포함) |
교훈
- pg_dump는 서버보다 같거나 높은 버전이어야 한다. PG17→PG16 마이그레이션이라도, 덤프 도구는 PG17이 필요하다.
- “버전 하나 차이”는 “그냥 되겠지”가 아니다.
transaction_timeout같은 신규 파라미터 하나가 복원을 막는다. - 데이터 이전 후 row count 검증은 필수다. 17개 테이블을 하나씩 세는 것이 귀찮아도, 나중에 “데이터가 빠졌다”를 디버깅하는 것보다 낫다.
- 프리티어는 공짜가 아니라, 직접 구축하는 대가를 지불하는 것이다. Railway의 Git push 한 번 vs Terraform + Docker + cron 설정. 시간과 비용의 트레이드오프.
- 프리티어 만료 후 계획을 미리 세워두면, 만료일에 당황하지 않는다.