본문으로 건너뛰기

클라우드 아키텍처 (CA)

이것을 실제 환경에 어떻게 배포할 것인가?

1. 전체 — 무엇이 어디에 있나​

그림이 말하는 것무엇
들어오는 길이 둘이다REST 는 Cloudflare 프록시를 지나고 실시간 호스트는 안 지난다
data 층에서 나가는 선이 없다라우팅 테이블에 기본 라우트가 없다. 문장이 아니라 그것이 강제 장치다
VPC 밖에 저장소가 없다회의록 간 관계까지 heymoa 안의 테이블이다. 밖으로 나가는 것은 벤더 호출뿐이다

2. 네트워크 — VPC · AZ · 서브넷​

AZ 를 나란히 놓고 층을 쌓는다. 이중화가 전제이므로 같은 것을 두 번 그린다 — "AZ 2개"라고 글로만 적으면 무엇이 두 벌이고 무엇이 한 벌인지 안 보인다. 이 그림만 개수를 그림으로 말한다.

그림이 말하는 것무엇
두 벌인 것NAT GW · app 층 · RDS 인스턴스 · Redis 노드
한 벌인 것ALB · IGW · Gateway Endpoint · ECS 클러스터
한쪽 AZ 는 항상 cross-AZ 다읽기 복제본을 두지 않기로 했으므로 양쪽 app 층이 같은 writer 에 쓴다. 장애 전환하면 그 한쪽이 바뀐다
S3 만 NAT 를 안 지난다나머지는 리전 서비스든 벤더든 전부 NAT 로 나간다

라우팅 테이블을 AZ 마다 나누는 것이 NAT 이중화의 실체다. 하나로 합치면 한쪽 NAT 가 죽을 때 양쪽 AZ 가 같이 죽고 평소에도 AZ 간 전송료가 붙는다.

무엇이 어느 경계에 속하나​

"AZ 밖에 있는 것"과 "AZ 를 걸치는 것"과 "AZ 안에 있는 것"은 다르다. 이걸 섞으면 이중화를 잘못 센다. 우리가 정한 규칙이 아니라 지켜야 하는 규칙이다.

경계무엇이 여기 사나
리전 · VPC 밖S3 · SQS · ECR · Secrets Manager · CloudWatch Logs · SES
VPC 전역 · AZ 없음IGW · 라우팅 테이블 · 보안그룹 · S3 Gateway Endpoint · ECS 클러스터
AZ 를 걸친다ALB — 각 public 서브넷에 노드가 하나씩 있다는 뜻이다
서브넷 = AZ 하나NAT GW(+EIP) · ECS 태스크 · RDS 인스턴스 · Redis 노드
AZ 목록만 갖는 것DB 서브넷 그룹 · 캐시 서브넷 그룹. 리소스가 아니라 명단이라 그림에 상자로 안 그린다

RDS 는 AZ 밖에 있지 않다. 인스턴스는 data 서브넷 하나 = AZ 하나에 산다. Multi-AZ 는 다른 AZ 의 서브넷에 standby 를 하나 더 두는 것이다.


3. 컴퓨트 — ALB · ECS​

그림이 말하는 것무엇
타깃 그룹이 둘뿐이다ai 는 ALB 뒤에 없다. 밖에서 도달할 길이 없다는 뜻이고, 그래서 서로를 어떻게 찾을지가 열려 있다
ECS 서비스가 셋이다조립은 ECS 에 없다. 뾰족하고 짧은 일이라 상시 태스크를 띄울 이유가 없다
서비스마다 확장 기준이 다르다api 는 CPU, realtime 은 활성 회의, ai 는 동시 스트림. 조립 함수는 Lambda 가 알아서 늘리고 우리는 상한만 정한다
realtime 만 축소를 끈다ECS 는 어느 태스크를 죽일지 우리가 고르게 해주지 않는다. 축소가 곧 회의 강제 종료다
드레이닝이 둘로 갈린다api 는 짧게, realtime 은 길게. 배포가 회의를 덮지 않게 하려는 것이 실행 단위를 가른 목적이다

realtime 은 CPU 도 요청 수도 확장 기준으로 안 맞는다 — 오디오 중계는 CPU 를 거의 안 쓰고, 2시간 회의도 요청으로는 1건이다.

배포는 회의를 덮을 수 없다​

드레이닝을 아무리 늘려도 회의가 그보다 길면 반드시 끊긴다. 회의 상한이 5시간이라 확실하다.

그래서 배포 전략은 "끊지 않는다"가 될 수 없고 "끊겨도 사용자가 모르게 복구한다"가 된다.

계획된 종료에서는 오디오 유실이 0이다. 드레이닝 안에서 끝나는 짧은 회의는 애초에 안 끊기지만 거기에 의존하지 않는다.

DB 마이그레이션​

  • expand-then-contract. 파괴적 변경은 앱 배포와 분리해 두 번에 나눠 올린다
  • 엔티티와 스키마가 어긋나면 컨테이너가 아예 안 뜬다. 마이그레이션 적용 뒤 옛 이미지를 올리는 경로가 막혀 있다
  • 되돌아가는 것은 앱뿐이고 마이그레이션은 적용된 채 남는다. 앞으로 고치는 것이 기본이다

4. 데이터 — 저장소와 가는 길​

그림이 말하는 것무엇
RDS 하나에 데이터베이스가 둘이다인스턴스는 하나인데 heymoa 와 heymoa_ai 가 산다. cross-database 쿼리 금지
S3 로 가는 길만 공짜다Gateway Endpoint 는 라우팅 테이블 항목이라 요금이 없다. 안 붙이면 같은 트래픽이 NAT 로 흘러 조용히 돈이 샌다
조립 함수만 VPC 밖에 있다RDS 를 안 부르니 VPC 에 넣을 이유가 없다. ENI·NAT·커넥션 풀이 이 함수에서 전부 빠진다
조립 함수는 Redis 에도 벤더에도 안 붙는다없다고 확인한 것이다
큐는 NAT 를 지난다SQS 는 리전 서비스이지만 VPC 밖이라 인터페이스 엔드포인트를 안 붙이면 NAT 로 나간다. 물량이 작아 지금은 NAT 로 시작한다

⚠️ app 라우팅 테이블 둘 다에 Gateway Endpoint 를 붙여야 한다. 하나만 붙이면 반쪽 트래픽이 조용히 NAT 로 나간다. 빠져도 에러가 안 나고 청구서에만 나타난다.

S3 수명 주기​

규칙값
오디오 세그먼트합본이 만들어지면 삭제 — 유예 기간 미정
Opus 압축본보존 — 기간은 법률 확인 후

「원본 PCM 7일」 규칙이 없어졌다. 브라우저부터 Opus 라 되돌릴 원본이 애초에 없다. 잃은 것을 명시한다 — 원본 품질로 재전사할 수 없다. 「미완료 멀티파트 Abort」도 없어졌다 — 멀티파트를 안 쓰므로 미완료 업로드가 생기지 않는다.

버킷 하나에 프리픽스로 나눈다 — 오브젝트 키가 회의 식별자로 시작해서 회의 삭제가 프리픽스 삭제 하나로 끝난다.


5. 관측과 보안​

그림이 말하는 것무엇
운영자가 인바운드 포트로 안 들어온다태스크가 밖으로 연결을 연다. 조립 함수가 아예 VPC 밖인 것과 같은 원리다
막는 것이 둘이다IAM 은 무엇을 할 수 있나, 보안그룹은 누구와 말할 수 있나. 둘을 섞으면 한쪽에 구멍이 난다
관측이 NAT 로 나간다트레이스 전량이 인터넷으로 나가므로 물량이 요금에 잡힌다

앱이 OTLP 로 Grafana Cloud 에 직접 보낸다. Collector 를 두지 않는다. 다만 부팅 실패와 크래시만 CloudWatch 에 짧게 남긴다 — 앱이 뜨기 전에 죽으면 OTLP 로 아무것도 안 나간다.

깨워야 하는 것은 "값이 없는 것"이다. 분석이 열 시간 0건인데 아무도 모르고, 관측 토큰이 만료됐는데 앱은 멀쩡히 돌고, 계측 수십 종이 열흘간 한 건도 도달하지 않는 일이 실제로 있었다. 신호가 끊긴 것을 감지하는 알람이 없으면 관측이 죽은 것을 관측할 방법이 없다.

⚠️ 죽은 잡을 보는 알람이 없다. 아무도 안 보면 없는 것과 같고 회의 하나가 영영 처리되지 않는다.


6. 연결 관계​

인바운드 — 밖에서 안으로​

#구간무엇이 허용하나
ⓐ사용자 → Cloudflare → VercelCloudflare 프록시
ⓑ사용자 → Cloudflare(프록시) → ALB → apialb-sg ← 인터넷 · api-sg ← alb-sg
ⓒ사용자 → ALB 직접 → realtime〃 · rt-sg ← alb-sg
ⓓpyannote → ALB → api · 서명 검증alb-sg · api-sg
ⓔ운영자 → SSM · ECS Exec인바운드 포트 없음. 태스크가 밖으로 연결을 연다

인바운드는 이 다섯뿐이고, 공개 경로는 ⓑ·ⓒ·ⓓ 셋이며, 전부 ALB 하나를 지난다.

⚠️ ⓓ(pyannote webhook) 가 api 로 온다. 이제 제출도 api 가 하므로 보내는 쪽과 받는 쪽이 같아졌다.

⚠️ ALB 의 인바운드를 Cloudflare 대역으로 좁힐 수 없다. ⓒ 가 DNS only 라 브라우저가 ALB 에 직접 붙는다. 오디오를 Cloudflare 로 보내지 않기로 한 결정의 대가다.

아웃바운드 — 안에서 밖으로​

#구간나가는 길물량
ⓞrealtime → S3 · 세그먼트 쓰기Gateway Endpoint — NAT 를 안 지난다월 ≈ 330 GB
ⓟrealtime → SonioxNAT (피할 수 없다)월 ≈ 330 GB
ⓠapi → pyannote · 제출NAT회의당 한 번. 바이트는 안 지난다
ⓡai → OpenAI · Linear · GitHubNAT토큰 물량 미정
ⓧ조립 함수 → S3 · SQSVPC 를 안 지난다회의당 합본 하나
ⓣapi → SES · Google OAuth2NAT작다
ⓤ전 서비스 → Grafana Cloud · LangfuseNAT트레이스 전량
ⓥ태스크 기동 → ECR · Secrets · CloudWatch LogsNAT배포·확장마다
ⓦSQS 발행과 소비NAT작다

⚠️ ⓟ 가 ⓞ 와 같은 크기인데 무료가 아니다. 같은 바이트를 S3 와 Soniox 양쪽에 보내는데 Soniox 는 인터넷 밖이라 Gateway Endpoint 로 뺄 수 없다. 다만 그 크기가 12배 줄었다 — PCM 384 kbps → Opus 32 kbps. 월 3.9 TB 였던 것이 ≈ 330 GB 다. ⚠️ ⓥ 가 NAT 를 지난다는 것은 "NAT 가 죽으면 새 태스크가 못 뜬다"는 뜻이다. NAT 를 AZ 마다 둔 이유가 이것이기도 하다.

무엇이 무엇을 막나​

전부 보안그룹 참조로 잇는다. 태스크 주소가 배포마다 바뀌므로 CIDR 로 쓴 규칙은 유지될 수 없다.

인터넷 ────> alb-sg ────> api-sg ────> ai-sg
└───> rt-sg ────┘
ai-sg ────> api-sg (콜백)

api-sg ────> rds-sg (heymoa)
rt-sg ────> rds-sg (heymoa)
조립 함수는 VPC 밖이라 SG 가 없다
ai-sg ────> rds-sg (heymoa_ai)

api-sg ────> redis-sg
rt-sg ────> redis-sg
ai-sg 는 Redis를 안 쓴다
규칙무엇이 강제하나
조립 함수가 DB 에 못 닿는다VPC 밖에 있어서 rds-sg 에 넣을 소스 자체가 없다. 문장이 아니라 이것이 강제 장치다
브라우저가 ai 에 도달할 수 없다ai-sg 가 api-sg·rt-sg 만 받는다. 타깃 그룹도 없다
data 층이 밖으로 못 나간다data 라우팅 테이블에 기본 라우트가 없다

⚠️ ⓛ 을 빠뜨리면 ai 가 기동하자마자 죽는다. heymoa-ai 는 같은 인스턴스의 다른 데이터베이스에 직접 붙는다.

권한이 곧 경계다​

태스크 역할을 서비스마다 나눈다. 문장으로 쓴 격리 규칙을 실제로 강제하는 것이 이것이다.

역할줘야 하는 것
api잡 발행·수신 · SES · Secrets · S3 GetObject 서명만(합본 presigned URL)
realtimeS3 PutObject(세그먼트) · Secrets
함수 실행 역할S3 읽기·쓰기 · 잡 수신·발행 · CloudWatch Logs. 그게 전부다. Secrets 가 없다
aiSecrets · 잡 수신

⚠️ S3 에 실제 바이트로 닿는 것은 realtime(쓰기)과 조립 함수(읽고 쓰기)뿐이다. api 에 GetObject 가 새로 생겼다 — 예전에는 worker 가 pyannote 에 제출해서 api 는 S3 권한이 0개였는데, 제출이 api 로 옮겨 오면서 presigned URL 을 서명할 권한이 필요해졌다. 바이트는 여전히 안 지난다. 함수 역할에 Secrets 가 없다 — 벤더 키를 안 갖는 것이 이 격리의 요점이다.


7. 기존 형상에서 넘겨받는 것​

전부를 새로 만드는 게 아니다.

지금어떻게 되나
VPC · 서브넷 넷재사용한다. 기존 private 둘을 app 층으로 올리고 data 층을 새로 판다
EC2 인스턴스와 그 안의 nginx없앤다. ECS 서비스와 ALB 가 대신한다
Single-AZ RDS제자리에서 Multi-AZ 로 올린다
붙어 있지 않은 S3 Gateway Endpoint새 라우팅 테이블에 붙인다
운영 접속SSH 인바운드를 두지 않는다. SSM Session Manager 로만 들어간다
Secrets Manager · SES · Cloudflare · Vercel그대로

8. 비용​

전부 추정이다. 실제 청구 단가는 확인하지 않았다.

항목월근거
STT egress (ⓟ)≈ $42오디오 330 GB × $0.126/GB
NAT 데이터 처리 — Soniox 몫 (ⓟ)≈ $15같은 330 GB × $0.045/GB. Gateway Endpoint 로 못 뺀다
조립 함수 호출작다회의 1,000건/일 × 수십 초
NAT 데이터 처리 — S3 몫 (ⓞ)$0 또는 $175Gateway Endpoint 를 붙였나 안 붙였나
S3 (원본 7일 + Opus 누적)12개월 뒤 ≈ $90원본을 영구 보관하면 47 TB · $1,070
ALB · NAT 시간요금 · Fargate · RDS · Redis미정단가 미확인
인터페이스 엔드포인트$0두지 않고 시작한다
LLM 토큰미정 — 계산 항목 자체가 없다이 규모에서 가장 클 수 있다

총액을 만들지 않는다. 근거 없는 숫자를 더하면 문서 전체가 무효가 된다.


9. 원칙 넷​

원칙
무엇을 어디에 두나정본은 RDS · 오디오는 S3 · 티켓과 팬아웃은 Redis · 회의록 간 관계는 RDS 안의 테이블 · 프로세스 경계를 넘는 잡은 큐, 같은 프로세스 안은 DB 행
관리 대상을 늘리지 않는다IaC 가 없다. 그래서 Lambda · 로그 사이드카 · NAT 인스턴스 · 인터페이스 엔드포인트를 지금은 전부 물렸다
되돌릴 수 없는 쪽을 피한다VPC 재사용도, expand-then-contract 마이그레이션도 같은 이유다
지금 안 만드는 것worker 서비스(영구히 — 조립은 함수다) · 그래프 전용 DB(영구히) · 인터페이스 엔드포인트 · 읽기 복제본