Dev V1 인프라 구성 - 100-hours-a-week/KTB4-2nd-Cloud GitHub Wiki
여담 V1은 App EC2 한 대와 AI Worker EC2 한 대로 운영합니다. 사진 업로드와 조회를 받는 서버와 AI 처리를 하는 서버를 나눴고, 원본 사진은 S3에 보관합니다.
서버를 나눈 이유와 규모 산정은 EC2 + Docker Compose 초기 배포 설계에 있습니다.
flowchart LR
U["사용자"] -. "DNS 조회" .-> R53["Route 53<br/>yeodam-2gether.com"]
U -- "HTTPS" --> EIP["Elastic IP"]
subgraph APP["App EC2 (t3.medium)"]
NG["Nginx<br/>80 / 443"]
FE["Frontend<br/>127.0.0.1:3000"]
BE["Backend<br/>127.0.0.1:8080"]
DB[("MySQL<br/>별도 EBS")]
NG --> FE
NG --> BE
BE --> DB
end
subgraph WK["Worker EC2 (c7i.xlarge)"]
AI["AI 컨테이너<br/>API, Worker, Qdrant"]
end
EIP --> NG
BE -- "8000<br/>App SG만 허용" --> AI
BE -- "원본 저장" --> S3[("S3<br/>원본 사진")]
AI -- "원본 조회" --> S3
인터넷에서 들어올 수 있는 곳은 App의 Nginx 80, 443뿐입니다. Frontend와 Backend는 App 안에서만 열려 있고, Worker의 8000번은 App Security Group에서 온 요청만 받습니다.
| App EC2 | Worker EC2 | |
|---|---|---|
| 인스턴스 |
t3.medium (CPU Credit unlimited) |
c7i.xlarge |
| 디스크 | Root gp3 30GiB, MySQL 전용 gp3 10GiB | Root gp3 30GiB |
| 실행 | Nginx, MySQL (Host), FE, BE (컨테이너) | AI 컨테이너 1개 |
| 외부 주소 | Elastic IP | 실행 중에만 Public IPv4 |
| 운영 | 상시 | 작업이 있을 때만 실행 |
| 접속 | SSM Session Manager | SSM Session Manager |
App은 한 대에서 Nginx, MySQL, FE, BE가 CPU를 같이 씁니다. CPU Credit이 떨어져 성능이 묶이면 서비스뿐 아니라 부하 테스트 결과도 흔들릴 수 있어서 unlimited로 두었습니다.
실제로 사진 150장 여행을 처리할 때 App CPU는 최대 81%까지 올랐고, 그때도 CPU Credit은 550 이상 남아 있었습니다.
Worker는 여행 한 건을 처리하는 동안 몇 분씩 CPU를 계속 씁니다. 그래서 버스트 계열 대신 c7i.xlarge를 골랐고, 150장 기준 AI 처리 약 3분 동안 CPU는 50% 안팎이었습니다.
| 데이터 | 위치 | 보존 방식 |
|---|---|---|
| 사용자, 여행, 사진 메타데이터 | MySQL (App의 별도 EBS) | App EC2를 교체해도 유지 |
| 원본 사진 | S3 trip-uploads/
|
Versioning, 삭제 방지 |
| 일괄 다운로드 ZIP | S3 trip-downloads/
|
1일 뒤 자동 삭제 |
| AI 벡터 | Worker의 Docker Volume | Worker 교체 시 함께 삭제 |
MySQL 데이터 EBS는 인스턴스와 별도 리소스로 만들고 Terraform에서 prevent_destroy를 걸었습니다. App EC2를 다시 만들어도 DB 디스크는 그대로 다시 붙일 수 있습니다.
S3는 공개 접근을 모두 막고, HTTPS가 아닌 요청은 거부합니다. App Role은 trip-uploads/를 읽고 쓰고 지울 수 있고, Worker Role은 읽기만 합니다.
flowchart LR
B["Browser"] --> N["Nginx"]
N -- "/api/trips*<br/>timeout 600s" --> BE["Backend"]
N -- "/api/*" --> BE
N -- "/*" --> FE["Frontend"]
처음에는 Backend가 루트 경로를 그대로 쓰고 있었습니다. 그런데 FE의 /auth/callback, /trips/create 같은 페이지와 BE의 /auth/csrf, /trips API가 같은 경로 아래에 섞여 있어서, Nginx에서 페이지별로 예외를 두어야 했습니다.
Backend 팀과 이야기해서 BE API 전체에 /api를 붙였고, 그 뒤로 Nginx 규칙은 /api/*와 나머지 두 갈래로 정리됐습니다.
/api/trips 경로만 timeout을 600초로 늘렸습니다. 사진 업로드의 마지막 요청은 AI 처리가 끝날 때까지 기다렸다가 응답하기 때문입니다. 업로드 한 요청의 크기 제한은 150m입니다.