클라우드 아키텍처 (CA)
2026-08-17 기록. 현재 문서와 내용이 다를 수 있다. 아래 내용은 당시 상태를 보존하며 수정하지 않는다.
이것을 실제 환경에 어떻게 배포할 것인가?
가장 아래층이자 가장 구체적인 문서다. 그대로 만들 수 있어야 한다 — 읽고 나서 무엇을 몇 개 띄워야 할지 모르면 덜 쓴 것이다.
- 집중하는 것 — Deployment / Infrastructure
- 담는 것 — VPC, ALB, ECS, RDS, Redis, S3, NAT, AZ, IAM 등
- 빼는 것 — UI 기능이나 Domain 구조
- 구체성 — 가장 구체적
IaC 를 두지 않는다. 형상 확인은 SSM 으로 접속해 CLI 로 한다. 그래서 이 문서가 형상의 정본이고, 관리 대상을 줄이는 선택이 그만큼 더 중요해진다.
주소를 적지 않는다. 층은 public / private app / private data 로, AZ 는 A / B 로만 구분한다. 실제 CIDR 은 콘솔에 입력할 때 필요한 값이지 구조를 읽는 데 필요한 값이 아니다.
| 그림 | 답하는 질문 |
|---|---|
| §1 | 무엇이 AWS 안이고 무엇이 밖인가 |
| §2 | 어느 서브넷에 무엇이 있고 어디로 나가나 |
| §3 | 몇 개가 뜨고 무엇으로 늘어나나 |
| §4 | 저장소가 어디 있고 누가 어떤 길로 붙나 |
| §5 | 무엇이 새어 나가고 무엇이 막나 |
1. 전체 — 무엇이 어디에 있나
10초 안에 읽혀야 하는 것 — 층이 셋이고, 저장소는 인터넷으로 못 나가며, 벤더는 전부 VPC 밖이라는 것.
| 그림이 말하는 것 | 무엇 |
|---|---|
| 들어오는 길이 둘이다 | 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 → Vercel | Cloudflare 프록시 |
| ⓑ | 사용자 → Cloudflare(프록시) → ALB → api | alb-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 → Soniox | NAT (피할 수 없다) | 월 ≈ 330 GB |
| ⓠ | api → pyannote · 제출 | NAT | 회의당 한 번. 바이트는 안 지난다 |
| ⓡ | ai → OpenAI · Linear · GitHub | NAT | 토큰 물량 미정 |
| ⓧ | 조립 함수 → S3 · SQS | VPC 를 안 지난다 | 회의당 합본 하나 |
| ⓣ | api → SES · Google OAuth2 | NAT | 작다 |
| ⓤ | 전 서비스 → Grafana Cloud · Langfuse | NAT | 트레이스 전량 |
| ⓥ | 태스크 기동 → ECR · Secrets · CloudWatch Logs | NAT | 배포·확장마다 |
| ⓦ | 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) |
realtime | S3 PutObject(세그먼트) · Secrets |
| 함수 실행 역할 | S3 읽기·쓰기 · 잡 수신·발행 · CloudWatch Logs. 그게 전부다. Secrets 가 없다 |
ai | Secrets · 잡 수신 |
⚠️ 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 또는 $175 | Gateway 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(영구히) · 인터페이스 엔드포인트 · 읽기 복제본 |
10. 아직 안 정한 것
- api ↔ ai 도달 수단 — 안 정하면 배포마다 깨진다
- VPC 재사용 · 서브넷 · 라우팅 · 보안그룹 · 엔드포인트
- LLM 비용 — 계산 항목이 없다
- 인스턴스 클래스 · 태스크 사양 · 캐시 노드 타입
- 조립 함수의 예약 동시성 · 메모리 · 타임아웃
- realtime 컨테이너 포트 · ALB 헬스체크 경로
- SQS 재시도 횟수 · 가시성 타임아웃 · DLQ 경보 임계
- 드레이닝 최댓값 · 종료 유예 상한
- S3 보존 기간 (법률 확인 후)