Cloud Architecture — HeyMoa
갱신일 2026-08-10 · 기준: 운영 실측(Linear 「AWS (아키텍처 / 비용 / 성능 / 권한)」) + 두 서비스
deploy/설정 이 문서가 갖는 것: 어디에 무엇이 떠 있나, 어떻게 올라가나, 무엇을 의도적으로 두지 않았나, 얼마가 드나. 여기 없는 것: 런타임 경계(system-architecture.md) · 모듈 배치(application-architecture.md).
⚠️ 정본 주의 —
heymoa-infra(Terraform)는 legacy이며 운영에 쓰지 않는다. 문서 일부가 아직 이 저장소를 운영 소유자로 지목하지만, 실제 형상은 아래와 다르다(§8). 이 문서는 실제 운영 형상을 정본으로 삼는다. IaC는 존재하지 않는다.
1. 배포 형상
한 장에 넣으면 노드 25개에 화살표가 스무 개 넘게 얽힌다. 요청이 지나가는 길 · AWS 안 · 밖으로 나가는 것 셋으로 나눈다.
1.1 요청이 지나가는 길
heymoa-ai 앞에는 nginx 가 없다. server 가 사설 IP 로 직접 8000 포트를 부른다 — 그래서 애플리케이션 인증이 없는 채로도 지금까지 굴러왔다(§4).
1.2 AWS 안 — ap-northeast-2 (서울)
EC2 두 대가 서로 다른 AZ 에 있는데 RDS 는 Single-AZ 다. 가용성 이득이 없고 AZ 간 통신 비용만 생긴다 — 의도한 배치가 아니다(§9).
SSH 포트를 열어두지 않는다. 접속은 Session Manager 로만 한다.
1.3 밖으로 나가는 것
| 어디서 | 어디로 | 무엇 |
|---|---|---|
heymoa-app | ElevenLabs (WSS) | 실시간 전사 |
heymoa-app | SES → Cloudflare Email Routing | 초대 메일 · catch-all 수신 |
heymoa-ai | OpenAI | LLM 호출 |
heymoa-ai | Langfuse | 프롬프트·트레이스 관측 |
| 둘 다 | Grafana Cloud (OTLP http/protobuf) | 앱이 직접 push · 수집 에이전트 없음 |
배포 경로(GitHub Actions → SSM)는 §5에서 따로 본다.
2. 리소스 표
| 리소스 | 값 |
|---|---|
| 리전 · 계정 | ap-northeast-2 (서울) |
| VPC | heymoa-vpc / vpc-00a27b208cb814690 / 10.30.0.0/16 |
| Subnet | Public1 10.30.0.0/20(2a) · Public2 10.30.16.0/20(2b) · Private1 10.30.128.0/20(2a) · Private2 10.30.144.0/20(2b, DB 서브넷 그룹) |
| EC2 #1 | heymoa-app / i-0364e2cd1bbbd3ad8 / t4g.small ARM64 Ubuntu 24.04 / AZ 2a / EIP 43.203.49.159 / 사설 10.30.12.229 / gp3 20GiB |
| EC2 #2 | heymoa-ai / i-08691b15dcf1359d5 / t4g.micro (1GB) / AZ 2b |
| Security Group | heymoa-app-sg (80 · 443 · 22 ← 0.0.0.0/0) · heymoa-ai-sg (8000 ← heymoa-app-sg에서만) · heymoa-database-sg (5432 ← app-sg) |
| RDS | heymoa-postgres / PostgreSQL 18.4 / db.t4g.micro / Single-AZ / gp2 20GiB 암호화 / 자동 확장 최대 1000GiB / Performance Insights 7일 |
| IAM | heymoa-ec2-role — 인라인 heymoa-server-secret-read · heymoa-ses-send, 관리형 CloudWatchAgentServerPolicy |
| Secrets Manager | heymoa/server/prod · heymoa/ai/prod · rds!db-6e699368-… (7일 자동 회전) |
| SES | 발신 [email protected] · IAM Role 인증(액세스 키 아님) · DKIM 3레코드 + DMARC |
| VPC Endpoint | heymoa-vpce-s3 — 생성만 되어 있고 쓰지 않는다 (S3 버킷 없음) |
| 접속 | SSM Session Manager (aws ssm start-session) |
| DNS · 메일 | Cloudflare — heymoa.app, Email Routing catch-all, 리다이렉트 룰 |
| 프런트 호스팅 | Vercel (heymoa-web) |
두 DB는 같은 RDS 인스턴스 안의 서로 다른 database다 — heymoa(원본) · heymoa_ai(파생, CREATE EXTENSION vector). cross-database 쿼리는 금지다.
3. 의도적으로 두지 않은 것
| 없는 것 | 대신 | 아꼈다고 계산한 것 |
|---|---|---|
| NAT Gateway | EC2를 public subnet에 둔다 | 월 $47+ |
| ALB | EC2 안의 nginx가 ALB 역할, Certbot이 HTTPS | 월 $16 + 퍼블릭 IPv4 2개 $7 |
| Terraform / CDK | 콘솔 + 수기 설정 | — |
| S3 버킷 | 아직 필요가 없다 (VPC Endpoint만 만들어 둠) | — |
| CloudWatch Logs | 도입했다가 제거 (비용이 아니라 일원화 때문). Grafana Cloud로 단일화 | 되돌리려면 systemctl start amazon-cloudwatch-agent 한 줄 |
| Grafana Alloy (Collector) | 앱이 OTLP 게이트웨이로 직접 push | — |
| Multi-AZ RDS · 읽기 복제본 | Single-AZ | — |
nginx 설정은 어느 저장소에도 없다. EC2에서 수기로 관리한다 — 인스턴스를 잃으면 같이 잃는다.
4. 네트워크 경로와 격리
사용자 ──HTTPS──> Cloudflare DNS ──> Vercel (heymoa.app · 프런트)
사용자 ──HTTPS──> Cloudflare DNS ──> EIP ──> EC2 heymoa-app : nginx : 443
└─> heymoa-server : 8080
heymoa-server ──사설 IP 10.30.12.x : 8000──> heymoa-ai (nginx를 거치지 않는다)
heymoa-server / heymoa-ai ──5432──> RDS (private subnet)
운영자 ──SSM Session Manager──> EC2 (SSH 키 없음)
- public edge는 nginx 하나뿐이다. heymoa-ai는 도메인이 없고 사설 IP로만 도달한다
- heymoa-ai의 8000은
heymoa-ai-sg가heymoa-app-sg에서 오는 것만 허용한다. system-architecture.md §4의 "server → ai 인증 없음"을 실제로 막고 있는 것이 이 보안그룹 하나다 — 애플리케이션 레벨 방어는 없다 - heymoa-server는 8080을 루프백에 묶지 않는다. heymoa-ai가 다른 EC2에서 붙어야 하기 때문이고, 인터넷 노출은
heymoa-app-sg가 막는다 - ⚠️
heymoa-app-sg가 22를0.0.0.0/0에 열어 두고 있다. 접속 수단은 SSM이므로 이 규칙은 쓰이지 않는다 — 닫을 수 있다
⚠️ EC2를 재생성하면 사설 IP가 바뀐다. 그때 세 값을 같이 맞춰야 한다 — ai의
SERVER_BASE_URL, server의ANALYSIS_AI_BASE_URL·ANALYSIS_AI_CALLBACK_BASE_URL. scheme·host·port가 정확히 같지 않으면 분석 요청이 전부 400으로 거절된다.
5. 배포 경로
| 서비스 | 방식 | 롤백 |
|---|---|---|
| heymoa-ai | dev push → Actions → SSM → deploy.sh | ✅ 헬스체크 60초 실패 시 이전 이미지로 자동 복귀. 빌드 전에 현재 이미지 ID를 잡아 둔다 (빌드가 같은 태그를 덮어쓰므로) |
| heymoa-server | 수동. EC2에서 git pull → docker build → compose up | ❌ 없다. ddl-auto: validate라 마이그레이션 적용 뒤 옛 JAR을 올리면 컨테이너가 아예 안 뜬다 |
| heymoa-web | Vercel 자동 | Vercel 이전 배포 |
되돌아가는 것은 앱뿐이다. 마이그레이션은 적용된 채 남는다 — 자동 downgrade는 데이터를 지울 수 있어 사람이 판단한다. 파괴적 마이그레이션(컬럼 삭제·타입 변경)은 앱 배포와 분리해 두 번에 나눠 올린다.
⚠️ APP-321 — GitHub Actions 과금 차단으로 CI/CD 경로가 멈춰 있다. heymoa-server는 "CI 일시 중지, 로컬 게이트"가 현재 상태다.
설정과 시크릿
- 앱 설정은 Secrets Manager에 JSON 하나(
heymoa/server/prod,heymoa/ai/prod). 저장소에.env.prod를 두지 않는다 — 파일은 EC2를 잃으면 같이 잃는다 - DB 자격증명은 여기 없다. RDS 관리형 시크릿이 7일마다 회전하고 앱이 기동할 때마다 읽는다. 회전 뒤 연결이 깨지면
docker restart하나로 회복된다 - AWS 자격증명은 EC2에 저장하지 않는다. SDK 기본 체인이 IMDS에서 임시 자격증명을 받는다
- ⚠️ JSON
null을 넣으면 안 된다. 변환이KEY=null이라는 문자열을 만들고 compose의${VAR:?}가 통과시킨다.INTERNAL_API_TOKEN이 그렇게 되면 서버가"null"을 공유 시크릿으로 쓰면서 정상 기동한다. 비울 키는 빈 문자열로 - ⚠️
GRAFANA_CLOUD_OTLP_TOKEN만docker restart로 안 바뀐다. 앱이 아니라deploy.sh가 읽어 컨테이너 환경변수로 넣기 때문 —opentelemetry-instrument가 프로세스 시작 시점에 읽는다. OTLP가 401로 계속 실패하는데 앱은 멀쩡히 도는 모양이 되고, 관측이 죽은 것을 관측할 방법이 없다
6. 관측
앱이 OTLP 게이트웨이로 직접 push한다. Collector(Alloy)를 두지 않았다.
| 항목 | 값 |
|---|---|
| 대상 | Grafana Cloud Free — 메트릭 10K active series · 로그/트레이스/프로파일 각 50GB/월 · 보관 14일 |
| 프로토콜 | http/protobuf (게이트웨이가 gRPC를 받지 않는다) |
| 인증 헤더 | base64("인스턴스ID:glc_토큰") — glc_ 토큰을 그대로 넣으면 401 |
| 히스토그램 | base2_exponential_bucket_histogram — 기본 explicit bucket은 (라우트 × 메서드 × 코드)마다 18 series를 만들어 한도를 태운다. Java는 대문자, Python은 소문자여야 한다 |
| 트레이스 샘플링 | 100% (실측 전까지 낮추지 않는다 — 낮추면 에러 트레이스도 같이 깎인다) |
| 파일 로그 | /var/log/heymoa/app.log · /var/log/heymoa-ai/app.log — 포렌식 사본이지 Grafana로 백필되는 경로가 아니다 |
감수한 것: 로그 유실 가능성 · tail sampling 불가(= "에러만 100% 보존"을 구현할 수 없다) · 호스트 메모리·디스크 메트릭 공백 · 크레덴셜 분산 · EC2별 egress 개별 부담.
Alloy를 다시 볼 기준 — 파일에만 로그가 있는 상황 2회 / egress 월 $20 초과 / 서비스 3개 이상 또는 k8s / 호스트 메모리·디스크 장애 / 에러 트레이스 보존이 병목.
⚠️ 두 번 깨진 적 있다 — ① 메트릭 temporality를
delta로 두자 배치 하나가 400이 되면서 그 안의 HTTP 메트릭까지 통째로 버려졌다. ②OTEL_INSTRUMENTATION_MICROMETER_ENABLED기본값이 false라 손으로 만든 계측 15종이 열흘간 한 건도 도달하지 않았다.
7. 비용과 트래픽
트래픽이 없을 때 월 약 $41.93 — EC2 $15.18 + EBS $1.82 + EIP $3.65 + RDS $18.25 + gp2 $2.62 + Secrets Manager $0.40. 그 밖에 Cloudflare 도메인(₩21,546/년) · Vercel · Grafana Cloud Free · OpenAI · ElevenLabs · Langfuse.
⚠️ egress가 다음 비용 항목이다 — heymoa-server → ElevenLabs가 시간당 약 230MB ≈ 월 165GB. AWS 무료 100GB를 이미 넘는다 (인터넷 $0.126/GB, cross-AZ $0.01/GB). 두 EC2가 서로 다른 AZ에 있어서 server ↔ ai 트래픽도 cross-AZ로 과금된다.
리소스 여유: heymoa-ai 실측 184MiB / 906MiB (20%). 방침은 "터지면 스펙업".
8. heymoa-infra(Terraform)와 실제 형상의 차이
문서 일부가 heymoa-infra@0c6f7b0를 운영 소유자로 지목하지만 실제로 쓰지 않는다. 그 코드를 그대로 적용하면 아래가 달라진다.
| 항목 | Terraform이 말하는 것 | 실제 운영 |
|---|---|---|
| EC2 | 1대 (t4g.small) | 2대 (t4g.small + t4g.micro) |
| Public subnet | 10.30.0.0/24 1개 | /20 2개 (AZ 2개) |
| 인바운드 | 80·443만, 22 없음 | 80·443·22 열림 |
| RDS | heymoa-prod-postgres · db.t4g.small · gp3 30GiB | heymoa-postgres · db.t4g.micro · gp2 20GiB · PG 18.4 |
| DB | heymoa 하나 | heymoa + heymoa_ai |
| heymoa-ai | 리소스 없음 | EC2 1대 |
| 상태 관리 | S3 backend + lockfile | 없음 |
| RDS 삭제 방어 | deletion_protection=false, skip_final_snapshot=true | 콘솔 설정 미확인 |
⇒ IaC를 되살릴 거라면 현재 형상을 import하는 것부터다. 지금 그 코드는 문서로도 정확하지 않다.
9. 미확인 · 열려 있는 것
| 무엇 | 지금 아는 것 |
|---|---|
| IaC 부재 | 인스턴스를 재생성하면 IAM 연결·에이전트 설치·nginx 설정이 전부 수작업이다 |
| RDS 삭제 방어 | 실제 deletion_protection · skip_final_snapshot 값이 확인되지 않았다. 실사용자 데이터가 생기기 전에 봐야 한다 |
| Grafana 대시보드·알림 | 토큰이 write 전용이라 조회로 확인하지 못했다. 사람이 봐야 한다 |
| CI 복구 (APP-321) | Actions 과금 차단이 언제 풀리는지 미정. 그동안 server는 로컬 게이트뿐 |
| Langfuse 운영 키 | 키를 넣으면 전사 원문이 외부로 나간다. 운영에서 켜져 있는지 확인 필요 |
| AWS Local Zone (LA) 배포 (APP-366) | Todo. 이슈 본문의 "부모 리전: 오레곤"이 현재 서울 리전과 모순이다 — 회의 전사에서 자동 생성된 항목으로 보이며 신뢰도가 낮다. 검증 필요 |
| S3 · VPC Endpoint | 엔드포인트만 있고 버킷이 없다. 만들 계획이 있는지 미정 |
heymoa-transcription-worker 저장소 | PR 5건이 있다는 사실만 확인. 운영에 떠 있지 않다 |