운영·배포 문서
서비스가 어떻게 돌아가는지 한 장으로 정리한 개요입니다. 자격 증명, 토큰이 든 주소, 계정 정보는 이 문서에 넣지 않습니다.
구성
| 부분 | 무엇 | 하는 일 |
|---|---|---|
| 웹·API | Cloud Run 서비스 n8-api (서울, 최소 인스턴스 0) | 모든 화면과 API. 참여·관리·결과 페이지를 서버에서 그립니다. |
| 정규화 | Cloud Run Job n8-normalize | 클립 하나를 1080×1920 공통 규격으로 다듬고 자막을 넣습니다. |
| 렌더 | Cloud Run Job n8-render | 풀버전·전체 상영본(재인코딩 없이 이어 붙이기)과 릴스(새로 인코딩)를 만듭니다. |
| 스케줄러 | Cloud Scheduler n8-deadline | 1분마다 내부 tick을 부릅니다. 스케줄러 인증 토큰이 없는 호출은 거절합니다. |
| DB | Postgres (Neon 무료 플랜) | 프로젝트·챕터·참여자·클립·렌더 행과 작업 큐. 고정비 없음. |
| 파일 | Cloudflare R2 (위치 힌트 APAC) | 원본, 정규화본, 결과 영상. 브라우저가 사전서명 URL로 직접 올리고 받습니다. |
서비스와 Job 두 개는 같은 컨테이너 이미지를 씁니다. 작업 큐는 DB 테이블이고, 디스패처가 DB 락으로 슬롯을 잡아 Job 실행을 띄웁니다.
처리 흐름
- 생성 —
POST /projects. 관리 토큰과 참여 슬러그를 만들고 바로collecting이 됩니다. - 업로드 — 참여 화면이 업로드 URL을 받아 R2에 직접 PUT합니다. 이름·필수 동의·브라우저 토큰이 없으면 URL을 주지 않습니다.
- 완료 콜백 — 브라우저가 완료를 알리면 R2 객체를 확인하고 클립을
processing으로 바꾼 뒤 정규화 작업을 큐에 넣습니다. 이 응답 뒤에 참여 화면이 “받았어요”를 띄웁니다. - 정규화 — Job이 원본을 받아 ffmpeg로 다듬고 정규화본을 R2에 올린 뒤
ready로 바꿉니다. 참여 화면은 상태를 보다가 “올라갔어요”로 바꿉니다. - 마감·렌더 요청 — 마감 tick 또는 영상 만들기가 올리기를 닫고 렌더 행을
queued로 넣습니다. 렌더 세대는 프로젝트마다 하나뿐입니다. - 렌더 — 챕터별 풀버전 → 전체 상영본 → 챕터별 릴스 순서로 만들고
done이 되면 프로젝트가ready가 됩니다. - 공개 —
ready이고 전체 상영본이 있을 때만 됩니다. 결과 슬러그를 만들고 보관 기한(14일·30일)을 기록합니다. - 보관 정리 — 기한이 지나면 tick이 R2와 DB를 정리합니다. 보관 기간 참고.
매분 작업 (tick)
스케줄러가 1분마다 POST /internal/tick을 부릅니다. 한 번에 하는 일:
- 마감 시각이 지난
collecting프로젝트를closed로 바꿉니다. 렌더 방식이deadline이면 같은 트랜잭션에서 렌더를 큐에 넣습니다. - 큐를 보고 빈 슬롯만큼 Job 실행을 띄우고, 끝난 실행을 거둡니다.
uploading으로 30분 넘게 멈춘 클립은failed로 두고 R2 조각을 지웁니다.running으로 3분 넘은 정규화 작업은failed(timed_out)로 둡니다. 다시 큐에 넣지 않습니다.running으로 45분 넘은 렌더 작업과 열린 렌더 행을failed로 닫습니다.- 보관 기한이 지난 프로젝트의 파일과 행을 정리합니다(단계별 tick당 20개). R2를 먼저 지우고 DB는 그 뒤 한 트랜잭션입니다. 실패하면 다음 tick에 다시 시도하고, 여러 번 돌아도 결과가 같습니다.
상태
프로젝트
collecting → closed → (렌더) → ready → revealed → expired
closed·ready는 마감을 미래로 옮기면collecting으로 돌아갑니다.ready에서 다시 열면 기존 렌더는 모두stale이 됩니다.revealed뒤에는 다시 열 수 없습니다. 순서를 바꾸면 그 챕터와 상영본만 다시 렌더하고 상태는 그대로입니다.
클립
uploading → processing → ready | rejected | failed → deleted
거절·실패 사유: too_short(5초 미만), unsupported(영상 없음), decode_error, timed_out, upload_expired.
렌더
queued → running → done | failed, 그리고 stale → deleted
재오픈이나 자동 볼륨 변경이 있으면 그 클립이 든 done 렌더는 stale이 되어 화면과 다운로드에서 빠집니다. 같은 종류의 새 렌더가 done이 되면 이전 done·stale은 deleted가 되고 파일도 지웁니다.
주소 규칙
| 주소 | 화면 | 접근 |
|---|---|---|
/ | 프로젝트 만들기 | 누구나 |
/admin/{관리 토큰} | 관리 화면 | 관리 토큰. 없거나 참여 슬러그면 403 |
/j/{참여 슬러그} | 참여 화면 | 링크를 아는 사람, noindex |
/r/{결과 슬러그} | 결과 화면 | 공개 뒤 링크를 아는 사람, noindex. 공개 전 404, 보관 끝 410 |
관리 API는 Authorization: Bearer 관리 토큰을 받습니다. 결과 슬러그는 참여 슬러그와 따로인 22자 무작위 값이고 공개할 때 만들어집니다. 결과 영상 다운로드는 R2 사전서명 URL로 302 리다이렉트하며, 주소가 렌더 id가 아니라 종류·챕터 기준이라 다시 만든 영상도 같은 링크로 나갑니다.
영상 규격
- 정규화본·결과: 1080×1920, 30fps 고정, H.264 High, yuv420p, BT.709, AAC 48kHz 스테레오. 결과는
+faststart. - 회전 메타데이터는 픽셀에 반영합니다. 가로 영상은 비율을 유지하고 블러 배경 위 가운데에 둡니다. HDR(HLG·PQ)은 BT.709로 맞춥니다. 오디오가 없으면 무음 트랙을 넣습니다.
- 음량: 2패스 loudnorm으로 −16 LUFS. 증폭 상한 +15 dB, +12 dB 넘게 키우면 관리 화면에 잡음 클 수 있음, −31 LUFS보다 작으면 너무 작음. 자동 볼륨을 끈 클립은 원본 음량 그대로입니다.
- 자막:
이름 · 정보. 한 줄에 안 들어가면 이름은 첫 줄, 정보는 아래 줄에 작게. 글꼴은 Pretendard(OFL), 이모지는 Noto Emoji. - 릴스: 동의자만 거른 뒤 1인 2초(정규화본 0.5~2.5초 구간), 최대 15명·30초. 15명을 넘으면 처음·끝을 고정하고 고르게 뽑습니다. 3명 미만이면 만들지 않습니다. 첫 컷에 주인공 이름과 주제 문구 타이틀.
용량·시간
| 항목 | 값 |
|---|---|
| 프로젝트 상한 | 주인공 10, 참여자 20(살아 있는 클립이 있는 사람만), 살아 있는 클립 200 |
| 클립 | 5~15초, 100MB 이하(브라우저에서 먼저 거름) |
| 업로드 URL 발급 제한 | IP당 10분 60회, 프로젝트당 1시간 300회. 넘으면 429 |
| 동시 실행 | 정규화 최대 7, 렌더 최대 1(별도 슬롯). 서울 리전 할당량 20 vCPU / 40 GiB 안에 API까지 맞춘 값 |
| 목표 시간 | 완료 콜백 → ready 2분 이내(20명 동시). 전체 렌더: 클립 60개 10분, 200개 20분 |
| 실측(2026-11-02, 실기기 클립 섞음) | 업로드 시작 → ready: 클립 60개 약 4.5분(그중 렌더 Job 약 1.9분), 200개 약 14분(렌더 Job 약 6.4분) |
배포·롤백
- 컨테이너 이미지 하나를 빌드해 서비스
n8-api와 Jobn8-normalize·n8-render를 같은 이미지로 함께 바꿉니다. 이미지 태그는 작업 번호와 커밋 해시입니다. - 배포 때 이미지 말고는 바꾸지 않습니다: CPU·메모리, 환경 변수, 시크릿 참조, 서비스 계정, 스케줄러, 최소 인스턴스 0.
- DB 마이그레이션은 API가 시작할 때 적용됩니다.
- 롤백: 서비스 트래픽을 이전 리비전으로 돌리고, Job 두 개의 이미지를 이전 태그로 되돌립니다. 이미 지운 R2 파일은 되돌릴 수 없습니다.
- 배포 뒤에는 확인 스크립트가
zz-로 시작하는 테스트 프로젝트를 만들어 실제 흐름을 돌리고, 끝나면 행과 파일을 지운 뒤 남은 수를 기록합니다. - CPU·메모리·최대 인스턴스를 올릴 때는 리전 할당량 표를 다시 계산합니다. 슬롯 수는 이미지 안 상수라 Job 설정으로 바뀌지 않습니다.
자격 증명
- DB 접속 정보와 R2 키는 저장소와 이미지에 넣지 않습니다. Secret Manager에 두고 서비스와 Job이 최신 버전을 읽습니다.
- 서비스·Job은 전용 런타임 서비스 계정으로, 스케줄러는 별도 서비스 계정의 OIDC 토큰으로 호출합니다. 재배포 때 이 설정을 바꾸지 않습니다.
- 로컬 실행에는 DB 주소와 R2 접속 값이 환경 변수로 필요합니다. 값은 저장소에 없습니다.
- 이 문서 사이트에는 토큰이 든 주소, 키, 계정 식별자를 적지 않습니다.
비용
- 유휴 비용: 인스턴스 0, 스케줄러 1개(무료 한도 안), Neon 무료 플랜, R2 무료 10GB 안쪽. 시크릿 보관과 매분 tick을 더해 대략 월 $2 미만(추정).
- 예산 알림은 월 $10입니다.
- R2는 송출 요금이 없어서 삭제 직전에 다운로드가 몰려도 비용이 늘지 않습니다.
알려진 한계
- 보관 기한이 지나는 순간 돌던 렌더 Job이 끝나며 파일을 올리면 그 파일이 남을 수 있습니다(클립이 먼저 지워져 보통은 실패).
- 참여자 이름·정보 행은 자동 삭제 대상이 아닙니다.
- DB를 쓰는 테스트는 운영 DB를 함께 쓰므로 한 번에 하나씩 돌립니다.
- −31 LUFS보다 작게 찍힌 원본을 업로드 단계에서 막거나 재촬영을 안내하는 기능은 출시 뒤 점검 항목입니다.