프로젝트/EWHA 캡스톤 졸업프로젝트

[졸업프로젝트] handDoc 배포 환경 구축 정리 (AWS EC2 + Docker + Nginx + FastAPI + Spring Boot) + 트러블슈팅

rngPwns 2025. 11. 23. 21:01

 

   handDoc 졸업프로젝트의 실제 배포 환경 구성 과정을 정리해보겠다.

   현재 나의 드림잡인 클라우드 엔지니어와 관련하여 배포를 많이 맡아보고 싶었고, 그 결과 AWS IaaS 환경에서 1번, NCP 쿠버네티스 환경에서 1번으로 총 두 번 서로 다른 방식으로 배포해보는 경험을 했다. FE도 배포를 맡아, Vercel로 배포했으며, 이후 여력이 되면 AWS S3 + CloudFront 기반으로 정적 호스팅도 구현해볼 계획이다.


   서비스는 React(Vercel) + Spring Boot + FastAPI + Nginx + RDS(MySQL) + MongoDB Atlas로 구성되며, 모든 백엔드, AI 서버는 단일 EC2(t3.micro) 위에서 Docker 기반으로 운영한다.

 

   AI까지 하려면 인스턴스를 2개 써야 하나 고민이었는데, 일단 1개로 사용해보기로 결정했고 결국 1대로 잘 돌아가서 좋은 결정이었지 싶다 ㅎㅎ

1. 배포 목표

  • 프리티어 EC2(t3.micro) 1대로 전체 서비스 운영
  • 모든 서비스 Docker 컨테이너화
  • Spring Boot는 Blue-Green 무중단 배포
  • FastAPI는 추론 서버라 단일 배포로 운영
  • MongoDB는 EC2 컨테이너 대신 MongoDB Atlas 사용
  • Nginx로 Reverse Proxy + HTTPS 적용

2. 시스템 아키텍처

   핵심 구조는 다음과 같다:

사용자
 ↳ Vercel(React)
   ↳ https://handdoc.store/
        ↳ Nginx(EC2)
             ├ /api → Spring Boot(Blue/Green)
             ├ /fastapi → FastAPI(Sign/Whisper)
             └ SSL/TLS termination
        ↳ RDS(MySQL)
        ↳ MongoDB Atlas

 

   Nginx가 모든 트래픽을 엔트리 포인트로 받고 경로 기반으로 컨테이너에 트래픽을 분기한다.

 

3. 서비스 엔드포인트

Spring Boot (Swagger)

프론트 → 백엔드 요청 예시

POST https://handdoc.store/api/v1/diagnosis/start
POST https://handdoc.store/api/v1/diagnosis/{id}/sign
POST https://handdoc.store/api/v1/diagnosis/{id}/speech
PATCH https://handdoc.store/api/v1/diagnosis/{id}/end
GET https://handdoc.store/api/v1/diagnosis/{id}/summary

FastAPI (Sign-to-Text WebSocket)

wss://handdoc.store/fastapi/ws

 

테스트:

npm i -g wscat
wscat -c wss://handdoc.store/fastapi/ws

 

초반에 handdoc.store 변경 이전 주소로 테스트한 결과

 

   아래와 같은 과정으로 SSL 인증 적용 + 도메인 연결하여 주소가 변경되었다!

//REST API Base URL
http://43.200.110.36/fastapi

//WebSocket URL
ws://43.200.110.36/fastapi/ws 
⇒ wss://handdoc.store/fastapi/ws

 

4. EC2 SSH 접근 및 폴더 구조

접속

ssh -i handdoc-key.pem ec2-user@43.200.110.36

디렉토리 구조

/home/ec2-user/apps/
└── .env
└── deploy.sh
└── docker-compose.spring-blue.yml
└── docker-compose.spring-green.yml
└── docker-compose.fastapi.yml

/etc/nginx/conf.d/
└── handdoc.conf
└── active-upstream.conf

 

5. Nginx 설정

Reverse Proxy 라우팅

/api/*     → Spring Boot  (8080)
/fastapi/* → FastAPI      (8000)

>> Nginx가 SSL 종료와 라우팅을 모두 담당한다. 무중단 배포 시 active-upstream.conf가 변경되며 nginx reload 수행!

 

5-1. EC2 보안그룹 설정 (Inbound Rules)

   단일 EC2 위에서 Nginx, Spring Boot, FastAPI를 모두 운영하기 때문에
아래 포트들을 인바운드로 열어주었다.

 

포트 용도
80 HTTP (Nginx)
443 HTTPS (SSL 인증서)
8080 Spring Boot Blue/Green API
8000 FastAPI 추론 서버
22 SSH (팀원 IP /32로 제한)

실제 설정 화면

8080 / 8000은 개발 단계에서는 전체 허용이지만,
운영 환경에서는 Nginx Reverse Proxy 뒤에서만 접근 가능하도록 좁히는 방향이 이상적이다.

 

6. Docker 로그 확인

Spring Boot

docker logs handdoc-spring-blue
docker logs handdoc-spring-green

FastAPI

docker logs handdoc-fastapi

Nginx

sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log

 

7. 실행 확인

현재 실행 중 컨테이너 확인

docker ps

   정상이라면 다음 둘이 항상 떠 있어야 한다:

  • handdoc-spring-blue 또는 handdoc-spring-green
  • handdoc-fastapi

 

8. Secrets / GitHub Actions 설정

핵심 Repository Secrets:

Key Value
DOCKER_USERNAME 보안상 생략
DOCKER_PASSWORD {Docker PAT}
EC2_SSH_HOST 43.200.110.36
EC2_SSH_USER ec2-user
EC2_SSH_KEY (handdoc-key.pem 내용)

 

>> Spring은 GitHub Actions로 Blue-Green 배포 자동화되며, FastAPI는 단일 Docker 배포로 구성된다.

 

9. RDS 설정(MySQL)

  • RDS: handdoc
  • 인스턴스: t3.micro
  • 초기 스토리지: 30GB
  • DB 접근: EC2 보안그룹 연결
  • 타입: MySQL / TCP

탄력적 IP: 43.200.110.36

 

10. 완성된 전체 인프라 구조

EC2(t3.micro)
 ├─ Nginx
 ├─ Spring Boot (Blue)
 ├─ Spring Boot (Green)
 └─ FastAPI (Sign/Whisper)

외부:
 ├─ Vercel(React FE)
 ├─ RDS(MySQL)
 └─ MongoDB Atlas

 

11. 트러블슈팅 — FastAPI 컨테이너 unhealthy 문제 해결

   배포 과정에서 FastAPI 컨테이너가 계속 unhealthy 로 떨어지는 문제가 있었다... 처음엔 코드 문제인가? Nginx 문제인가? 싶었는데, 원인은 전혀 다른 곳에 있었다.

문제 상황

   FastAPI 컨테이너가 올라오긴 하는데, 몇 초 지나면 상태가 healthy → unhealthy 로 바뀌었다... docker ps 명령어를 입력해보면 빨갛게 떠 있었다.

   따라서 팀원과 함께 원인을 계속 분석했다...

이렇게 onnx 경량화까지 생각해보고....

 

근데 결국 onnx 경량화 없이 해결하는 방법을 찾아내어 스스로 해결했당 ㅎ (뿌듯)

 

   내가 내린 결론은 다음과 같다.

  • AI 모델 로딩이 생각보다 오래 걸림 (TensorFlow Lite + MediaPipe 초기화)
  • Docker 기본 healthcheck timeout이 3초라서 모델 로딩 중에 /health 응답을 못 받음
  • 그 결과, 헬스체크 실패가 누적되어 FailingStreak = 30 → unhealthy 판정

   즉, FastAPI는 열심히 모델을 메모리에 올리고 있을 뿐인데 Docker 입장에서는 '이 컨테이너 대답 안 하네?' 하고 바로 잘못된 판정 내린 것이라고 볼 수 있다.

모델 로딩이 이렇게 오래 걸릴 줄은 솔직히 예상 못 했다…

 

   즉, FastAPI 자체 문제가 아니라 건강 체크가 너무 빨라서 AI 서버가 부팅을 다 하기 전에 '죽은 컨테이너'로 오해한 것이다.

  • 모델 로딩 시간: 7~15초
  • Docker healthcheck timeout: 3초
  • 그 사이에 /health 요청 → 응답 없음 → 실패 판단

그래서 매번 unhealthy가 뜬 거구..

 

해결 방법 — healthcheck 타이밍 넉넉하게 주기!

   해결은 간단했다. docker-compose.fastapi.yml 안의 healthcheck 시간을 FastAPI 특성에 맞춰 조정했다.

적용한 설정

interval: 30s      # 검사 간격 넓힘
timeout: 15s       # 응답 대기시간 여유줌
start_period: 60s  # 모델 로딩 시간 고려해 1분 대기

 

 

수정 후 결과

   healthcheck가 FastAPI의 'AI 모델 로딩 타임'을 기다려주기 시작했고 컨테이너는 정상적으로 healthy 상태를 유지했다.

또한 실제 Docker 로그에서도 모델 로딩 직후 바로 OK로 바뀌는 걸 확인했다.

결국 문제는 컨테이너가 아니라, 'AI 모델 초기화는 무겁다'는 걸 Healthcheck가 모르는 게 문제였다.

 

   AI가 들어간 컨테이너는 반드시 healthcheck 시간을 모델 로딩 시간 기준으로 맞춰야 한다는 걸 이번 기회에 배워가게 되었다.

healthy로 바꾸기 성공!

 

마무리

   이 배포 구조는 단일 EC2로 비용을 최소화하면서도,

  • 무중단 배포
  • AI 모델 추론
  • WebSocket
  • SSL
  • 프록시 라우팅
  • DB 분리

   까지 모두 포함한 효율적인 환경이라고 볼 수 있겠다~~!