Daeseon Yoo
Back to project
·Troubleshoot·6 min

Vercel + AWS 백엔드 배포기: RDS, S3, 그리고 부딪힌 벽들 순서대로

첫 실 백엔드 배포 — Terraform으로 RDS(us-east-1) + 비공개 S3 버킷을 만들고, 기존 Vercel 앱에 환경변수만으로 연결. 부딪힌 벽들을 순서대로: 프리티어 백업 한도, RDS SSL 핸드셰이크(sslmode=require는 verify-full), 카카오 KOE006(플랫폼 도메인 누락), KOE237 레이트 리밋, 그리고 OAuth 계정 분리.

앱은 프론트엔드 + API가 Vercel에 떠 있었지만 실 DB가 없었음 — 임베디드 PGlite로 돌아서 재시작하면 데이터가 날아감. 이번이 진짜 백엔드 첫 배포: Postgres는 AWS RDS, 미디어는 S3, Vercel엔 환경변수만으로 연결. 리전은 원래 계획한 서울 대신 us-east-1(버지니아) — 타깃 사용자가 북미로 바뀌어서.

Terraform, 손으로 직접

모듈 붙여넣기 대신 각 조각을 이해하려고 IaC를 처음부터 씀: provider(AWS, region는 변수), 기본 VPC와 서브넷을 찾는 data, 그리고 DB 서브넷 그룹·보안그룹(인바운드 5432만)·aws_db_instance(Postgres 16, db.t3.micro, 20GB gp3, 저장 암호화) resource. terraform initplanapply.

벽 1 — 프리티어 백업 한도

FreeTierRestrictionError: The specified backup retention period exceeds the maximum available to free tier customers.

backup_retention_period = 7이 프리티어 계정엔 너무 높음. 0으로 바꾸니 apply 통과. 엔드포인트: beside-db.…us-east-1.rds.amazonaws.com.

S3 + 최소 권한 IAM

aws_s3_bucket + aws_s3_bucket_public_access_block(네 옵션 전부 ON) 추가 — 완전 비공개, 공개 객체 URL 없음(미디어는 단기 서명 URL로 제공). 그리고 전용 IAM 유저 beside-s3-userAmazonS3FullAccess만 붙이고 액세스 키 발급 — 루트 dev_user를 재사용하지 않고, 앱 자격증명이 S3 외엔 못 건드리게(유출돼도 피해 범위 한정).

시크릿 + Vercel 환경변수

SESSION_SECRET·CRON_SECRET(crypto.randomBytes hex)와 VAPID 키쌍(web-push) 생성. 레포 연결(vercel link) 후 프로덕션 환경변수 전부 설정 — DATABASE_URL, S3_*, SESSION_SECRET, VAPID_*, CRON_SECRET, 그리고 기존 카카오/구글 키(개발과 동일 — OAuth 키는 앱 단위지 도메인 단위가 아님). vercel --prod로 재배포.

벽 2 — SSL 핸드셰이크 (제일 길었던 것)

로그인이 OAuth 콜백에서 실패. 처음엔:

[kakao callback] no pg_hba.conf entry for host "98.92.91.124", user "beside_admin", database "beside", no encryption

DATABASE_URLsslmode가 없어 pg가 평문으로 접속 → RDS가 거부. ?sslmode=require 추가. 새 에러:

[kakao callback] Connection terminated unexpectedly
[google callback] Connection terminated unexpectedly

"왜"가 없는 일반 메시지. 추측 대신 pg의 파싱을 로컬에서 그대로 재현 — 프로덕션 안 건드림:

require    => ssl = {}
no-verify  => ssl = {"rejectUnauthorized":false}

pg-connection-stringrequire/prefer/verify-caverify-full의 별칭으로 처리된다고 경고까지 띄움. 즉 sslmode=requirepg가 RDS 인증서를 Node 기본 CA 목록으로 검증하게 만듦 — 그런데 RDS 인증서는 Amazon RDS 루트 CA로 서명돼서 그 목록에 없음 → 검증 실패 → 연결 종료.

두 번째 테스트: URL이 sslmode=require인 동안엔 코드에서 ssl: { rejectUnauthorized: false }를 줘도 소용없음 — connection string의 sslmode가 명시적 옵션을 덮어씀(해석된 ssl이 여전히 {}). 그래서 이건 connection string에서 잡는 게 정답.

해결: DATABASE_URL?sslmode=no-verify(전송 암호화는 하되 CA 검증 생략). 코드 변경 없음. 더 엄격한 방법(나중)은 Amazon RDS CA 번들을 붙여서 풀 검증.

벽 3 — 카카오 KOE006

Admin Settings Issue (KOE006)

콜백은 요청 origin으로 redirect_uri를 만듦(lib/oauth/kakao.ts). 개발/localhost는 됐지만, 프로덕션에선 카카오 콘솔에 Web 플랫폼 사이트 도메인(https://beside.daeseon.ai)이 등록 안 돼 있었음 — Redirect URI만 등록해선 부족. 플랫폼 도메인 + 프로덕션 Redirect URI 등록 + 카카오 로그인 활성화. 키는 그대로(localhost와 prod가 같은 앱). 구글도 같은 세트 필요: Authorized redirect URI, Authorized JavaScript origins, OAuth consent screen 테스트 유저(또는 앱 publish).

벽 4 — KOE237 레이트 리밋

kakao token exchange 429: ... "token request rate limit exceeded","error_code":"KOE237"

친구가 테스트하다 만남. 콜백은 요청당 코드를 딱 한 번 교환하니 루프가 아님 — 카카오의 앱 단위 토큰 레이트 리밋임. SSL 디버깅 중 DB에서 실패한 시도들도 토큰 교환 단계는 통과(요청 소모)했고, 신규 앱은 쿼터가 낮으며, www/trailing-slash 리다이렉트가 끼면 로그인 1회당 요청이 2배가 됨. 시간 지나면 풀림.

OAuth 계정 분리라는 함정

카카오로 로그인했다가 구글로 로그인하니 서로 다른, 빈 것처럼 보이는 보관함 두 개가 나옴. 버그 아님: establishUser는 유저를 auth_identifier로 식별해서 kakao:<id>google:<id>는 서로 다른 유저 = 서로 다른 보관함. 자동 연결이 없는 건 앱이 의도적으로 이메일을 안 받기 때문(카카오: id+닉네임, 구글: openid profile만) — 합칠 공통 키가 없음. 진짜 account linking(검증된 이메일로 연결, 또는 수동 "계정 연결")은 MVP 범위 밖.

도착점

프론트는 Vercel, Postgres는 RDS(us-east-1), 미디어는 비공개 S3 버킷, 전부 환경변수로 연결 — 앱은 임베디드-PGlite-개발에서 실-인프라-프로덕션으로 코드 변경 없이 전환됨. 테이블은 첫 부팅에 앱의 CREATE TABLE IF NOT EXISTS DDL로 생성되니 수동 마이그레이션 단계 없음. SSL 모드와 카카오 플랫폼 도메인만 맞추면 로그인이 끝까지 됨.