본문으로 건너뛰기

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-appElevenLabs (WSS)실시간 전사
heymoa-appSES → Cloudflare Email Routing초대 메일 · catch-all 수신
heymoa-aiOpenAILLM 호출
heymoa-aiLangfuse프롬프트·트레이스 관측
둘 다Grafana Cloud (OTLP http/protobuf)앱이 직접 push · 수집 에이전트 없음

배포 경로(GitHub Actions → SSM)는 §5에서 따로 본다.


2. 리소스 표

리소스
리전 · 계정ap-northeast-2 (서울)
VPCheymoa-vpc / vpc-00a27b208cb814690 / 10.30.0.0/16
SubnetPublic1 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 #1heymoa-app / i-0364e2cd1bbbd3ad8 / t4g.small ARM64 Ubuntu 24.04 / AZ 2a / EIP 43.203.49.159 / 사설 10.30.12.229 / gp3 20GiB
EC2 #2heymoa-ai / i-08691b15dcf1359d5 / t4g.micro (1GB) / AZ 2b
Security Groupheymoa-app-sg (80 · 443 · 22 ← 0.0.0.0/0) · heymoa-ai-sg (8000 ← heymoa-app-sg에서만) · heymoa-database-sg (5432 ← app-sg)
RDSheymoa-postgres / PostgreSQL 18.4 / db.t4g.micro / Single-AZ / gp2 20GiB 암호화 / 자동 확장 최대 1000GiB / Performance Insights 7일
IAMheymoa-ec2-role — 인라인 heymoa-server-secret-read · heymoa-ses-send, 관리형 CloudWatchAgentServerPolicy
Secrets Managerheymoa/server/prod · heymoa/ai/prod · rds!db-6e699368-… (7일 자동 회전)
SES발신 [email protected] · IAM Role 인증(액세스 키 아님) · DKIM 3레코드 + DMARC
VPC Endpointheymoa-vpce-s3생성만 되어 있고 쓰지 않는다 (S3 버킷 없음)
접속SSM Session Manager (aws ssm start-session)
DNS · 메일Cloudflareheymoa.app, Email Routing catch-all, 리다이렉트 룰
프런트 호스팅Vercel (heymoa-web)

두 DB는 같은 RDS 인스턴스 안의 서로 다른 database다heymoa(원본) · heymoa_ai(파생, CREATE EXTENSION vector). cross-database 쿼리는 금지다.


3. 의도적으로 두지 않은 것

없는 것대신아꼈다고 계산한 것
NAT GatewayEC2를 public subnet에 둔다월 $47+
ALBEC2 안의 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-sgheymoa-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-aidev push → Actions → SSM → deploy.sh헬스체크 60초 실패 시 이전 이미지로 자동 복귀. 빌드 전에 현재 이미지 ID를 잡아 둔다 (빌드가 같은 태그를 덮어쓰므로)
heymoa-server수동. EC2에서 git pull → docker build → compose up없다. ddl-auto: validate라 마이그레이션 적용 뒤 옛 JAR을 올리면 컨테이너가 아예 안 뜬다
heymoa-webVercel 자동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_TOKENdocker 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이 말하는 것실제 운영
EC21대 (t4g.small)2대 (t4g.small + t4g.micro)
Public subnet10.30.0.0/24 1개/20 2개 (AZ 2개)
인바운드80·443만, 22 없음80·443·22 열림
RDSheymoa-prod-postgres · db.t4g.small · gp3 30GiBheymoa-postgres · db.t4g.micro · gp2 20GiB · PG 18.4
DBheymoa 하나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건이 있다는 사실만 확인. 운영에 떠 있지 않다