ESG 물리 기후 리스크 예측 프로젝트-인프라_서버

낭가인·2025년 12월 4일

SKALA최종프로젝트

목록 보기
5/9

프로젝트 개요

On-Do 팀은 기업의 사업장별 기후 물리 리스크를 예측하는 웹 서비스를 개발했다. Vue.js 프론트엔드, Spring Boot 백엔드, FastAPI AI 에이전트, 그리고 별도의 ModelOps 서버로 구성된 마이크로서비스 아키텍처를 채택했고, 이를 GCP(Google Cloud Platform)에 배포했다. 이 글에서는 왜 Kubernetes 대신 Docker + Nginx를 선택했는지, 그리고 Cloudflare, Nginx, Docker가 각각 어떤 역할을 하는지 실제 프로젝트 사례를 통해 설명하겠다.

전체 아키텍처

[사용자]
    ↓
[Cloudflare CDN + DDoS Protection]
    ↓
[DNS: on-do.site → GCP VM IP]
    ↓
[GCP VM Instance]
    ├─ [Nginx Proxy Manager] (:80, :443)
    │   ├─ on-do.site → polaris-frontend:80
    │   ├─ api.on-do.site → polaris-backend-java:8080
    │   └─ ai-agent-api.skax.co.kr → polaris-backend-fastapi:8000
    │
    ├─ [Docker Container: polaris-frontend] (Vue.js)
    ├─ [Docker Container: polaris-backend-java] (Spring Boot)
    ├─ [Docker Container: polaris-backend-fastapi] (FastAPI AI Agent)
    ├─ [별도 서버: ModelOps Server] (FastAPI ML Model Serving)
    └─ [PostgreSQL Database]

각 기술 스택의 역할

1️⃣ Cloudflare - CDN과 보안의 첫 번째 관문

역할:
DNS 관리: on-do.site 도메인을 GCP 서버 IP로 연결
무료 SSL 인증서: Cloudflare Proxy를 통해 HTTPS 자동 적용 (프론트엔드)
CDN: 정적 파일을 전 세계 엣지 서버에 캐싱하여 응답 속도 향상
DDoS 방어: 악의적인 트래픽 차단

우리 프로젝트에서:
프론트엔드(on-do.site)는 Cloudflare Proxy 사용 → CDN + 자동 HTTPS
백엔드(api.on-do.site)는 DNS only 모드 → Nginx에서 직접 SSL 처리
왜 백엔드는 Proxy를 끄나? Cloudflare Proxy를 켜면 Cloudflare가 중간에서 모든 요청을 처리하는데, 백엔드 API는 Nginx Proxy Manager에서 Let's Encrypt 인증서를 발급받아 직접 SSL을 처리하기 때문에 충돌이 발생한다.

2️⃣ Nginx Proxy Manager - 트래픽 라우팅의 중심

역할:
리버스 프록시: 도메인별로 요청을 적절한 Docker 컨테이너로 전달
SSL 인증서 관리: Let's Encrypt를 통해 무료 SSL 인증서 자동 발급 및 갱신
포트 매핑: 외부 80/443 포트를 내부 컨테이너의 다양한 포트로 연결
우리 프로젝트에서:
on-do.site:443
→ Nginx Proxy Manager
http://polaris-frontend:80

api.on-do.site:443
→ Nginx Proxy Manager
http://polaris-backend-java:8080

ai-agent-api.skax.co.kr:443
→ Nginx Proxy Manager
http://polaris-backend-fastapi:8000
설정 예시:
Domain: on-do.site
Scheme: http
Forward Hostname: polaris-frontend
Forward Port: 80
SSL Certificate: Let's Encrypt (자동 발급)
Force SSL: ON
왜 Nginx Proxy Manager를 쓰나?
GUI로 간편하게 설정 가능 (nginx.conf 직접 수정 불필요)
SSL 인증서 자동 갱신
여러 서비스를 하나의 서버에서 도메인별로 분리

3️⃣ Docker - 컨테이너 기반 배포

역할:
각 서비스를 독립적인 컨테이너로 실행
환경 변수 주입으로 설정 관리
이미지 기반 배포로 재현성 보장
우리 프로젝트에서:

프론트엔드 컨테이너

docker run -d \
  --name polaris-frontend \
  --network web \
  -p 80:80 \
  asia-northeast3-docker.pkg.dev/.../polaris-frontend:latest

백엔드 컨테이너 (Spring Boot)

docker run -d \
  --name polaris-backend-java \
  --network web \
  -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=prod \
  -e DB_HOST=10.117.192.3 \
  -e DB_PORT=5432 \
  -e JWT_SECRET=${{ secrets.JWT_SECRET }} \
  -e FASTAPI_API_KEY=${{ secrets.FASTAPI_API_KEY }} \
  asia-northeast3-docker.pkg.dev/.../polaris-backend-java:latest

AI Agent 서버 (FastAPI)

docker run -d \
  --name polaris-backend-fastapi \
  --network web \
  -p 8000:8000 \
  -e MODELOPS_API_URL=https://modelops.skax.co.kr \
  asia-northeast3-docker.pkg.dev/.../polaris-backend-fastapi:latest

Docker Network: 모든 컨테이너를 web 네트워크에 연결하여 컨테이너 이름으로 서로 통신:
docker network create web
이렇게 하면:
Spring Boot에서 http://polaris-backend-fastapi:8000로 AI Agent 호출
AI Agent에서 외부 ModelOps 서버로 ML 모델 추론 요청

4️⃣ 서비스 간 통신 구조

1. 사용자 요청 플로우:

[사용자]
→ "on-do.site에서 리스크 분석 요청"

[프론트엔드 Vue.js]
→ API 호출: POST https://api.on-do.site/api/analysis/start

[백엔드 Spring Boot]
→ 데이터베이스에 분석 작업 저장
→ AI Agent 호출: POST http://polaris-backend-fastapi:8000/predict

[AI Agent FastAPI]
→ ModelOps 서버 호출: POST https://modelops.skax.co.kr/inference
→ ML 모델로 리스크 예측
→ 결과를 Spring Boot로 반환

[백엔드 Spring Boot]
→ 결과를 데이터베이스에 저장
→ 프론트엔드로 응답

[프론트엔드]
→ 사용자에게 리스크 점수 시각화

2. 내부 vs 외부 통신:

내부 (Docker 네트워크): 컨테이너 이름으로 통신 (빠름, 보안)
polaris-backend-java → polaris-backend-fastapi
외부 (인터넷): 도메인으로 통신
polaris-backend-fastapi → modelops.skax.co.kr

5️⃣ 왜 Kubernetes를 쓰지 않았나?

Kubernetes의 장점:

자동 스케일링 (HPA: Horizontal Pod Autoscaler)
무중단 배포 (롤링 업데이트)
자가 치유 (Pod 장애 시 자동 재시작)
서비스 디스커버리, 로드 밸런싱
우리가 Kubernetes를 선택하지 않은 이유:

1. 프로젝트 규모가 작음

서비스 4개 (프론트엔드, 백엔드, AI Agent, ModelOps)
단일 VM 인스턴스에서 충분히 실행 가능
트래픽이 많지 않아 오토스케일링 불필요

2. 학습 곡선과 복잡도

Kubernetes는 다음을 모두 이해해야 함:

  • Pod, Deployment, Service, Ingress
  • ConfigMap, Secret
  • kubectl 명령어
  • YAML 설정 파일 작성
  • Helm 차트 (패키지 관리)
  • 클러스터 모니터링
    반면 Docker + Nginx는:
    docker run -d --name myapp -p 8080:8080 myimage
    이것만으로 배포 완료!

3. 비용

항목 Docker + Nginx Kubernetes (GKE)
컴퓨팅 e2-medium VM 1대
$25/월 마스터 노드 + 워커 노드 3대
$150~300/월
관리 비용 무료 (직접 관리) 클러스터 관리 비용 별도
네트워크 무료 (단일 VM) 로드 밸런서 비용 별도
우리의 선택: 3개월 프로젝트에 $300/월은 과하다!

4. 배포 파이프라인이 간단함

우리의 CI/CD (Docker + Nginx):

GitHub Actions

  1. 코드 푸시 (main 브랜치)
  2. Docker 이미지 빌드
  3. GCP Artifact Registry에 푸시
  4. SSH로 서버 접속
  5. docker pull && docker stop && docker run
  6. Health Check (60초 대기)
    만약 Kubernetes였다면:
  7. 코드 푸시
  8. Docker 이미지 빌드
  9. Registry에 푸시
  10. kubectl apply -f deployment.yaml
  11. Ingress 설정 업데이트
  12. 롤링 업데이트 모니터링
  13. Pod 상태 확인
  14. Service Mesh 설정 (Istio 등)
    📊 실제 배포 플로우
    CD 파이프라인 (GitHub Actions)
    name: CD - Deploy to Server
on:
  workflow_run:
    workflows: ['CI - Build & Push']
    types: [completed]
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-22.04
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    
    steps:
      - name: SSH로 서버 배포
        uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            # GCP Artifact Registry 인증
            echo '${{ secrets.GCP_SA_KEY }}' | docker login -u _json_key --password-stdin https://asia-northeast3-docker.pkg.dev
            
            # 최신 이미지 pull
            docker pull asia-northeast3-docker.pkg.dev/.../polaris-backend-java:latest
            
            # 기존 컨테이너 중지 및 삭제
            docker stop polaris-backend-java || true
            docker rm polaris-backend-java || true
            
            # 새 컨테이너 실행
            docker run -d \
              --name polaris-backend-java \
              --network web \
              --restart unless-stopped \
              -p 8080:8080 \
              -e SPRING_PROFILES_ACTIVE=prod \
              -e JWT_SECRET="${{ secrets.JWT_SECRET }}" \
              -e DB_HOST="${{ secrets.DB_HOST }}" \
              -e DB_PORT="${{ secrets.DB_PORT }}" \
              -e DB_NAME="${{ secrets.DB_NAME }}" \
              -e DB_USERNAME="${{ secrets.DB_USERNAME }}" \
              -e DB_PASSWORD="${{ secrets.DB_PASSWORD }}" \
              -e SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT=org.hibernate.dialect.PostgreSQLDialect \
              -e MAIL_HOST="${{ secrets.MAIL_HOST }}" \
              -e MAIL_PORT="${{ secrets.MAIL_PORT }}" \
              -e MAIL_USERNAME="${{ secrets.MAIL_USERNAME }}" \
              -e MAIL_PASSWORD="${{ secrets.MAIL_PASSWORD }}" \
              -e FASTAPI_API_KEY="${{ secrets.FASTAPI_API_KEY }}" \
              asia-northeast3-docker.pkg.dev/.../polaris-backend-java:latest
            
            # Health check (60초 타임아웃)
            echo "Health check 중..."
            for i in {1..60}; do
              if docker exec polaris-backend-java curl -f -s http://localhost:8080/actuator/health > /dev/null 2>&1; then
                echo "✓ 배포 성공! (${i}초 경과)"
                exit 0
              fi
              sleep 1
            done
            
            echo "✗ Health check 실패 (60초 타임아웃)"
            docker logs polaris-backend-java --tail 50
            exit 1

배포 시 주의사항:
환경 변수 누락 방지: GitHub Secrets에 모든 필수 환경 변수 등록
Health Check 필수: 컨테이너가 정상 시작되었는지 확인
기존 컨테이너 정리: docker stop && docker rm 후 새 컨테이너 실행

우리 아키텍처의 장단점

장점

1. 단순성

배포
docker run -d --name myapp myimage

로그 확인
docker logs myapp

재시작
docker restart myapp
명령어 몇 줄로 모든 게 해결됨. Kubernetes의 복잡한 YAML 파일 불필요.

2. 비용 효율

단일 VM: e2-medium (2 vCPU, 4GB RAM) → $25/월
Kubernetes: GKE 클러스터 최소 구성 → $150/월 이상
3개월 프로젝트 기준:
Docker + Nginx: $75
Kubernetes: $450
$375 절약!

3. 빠른 디버깅

컨테이너 내부 접속
docker exec -it polaris-backend-java bash

실시간 로그
docker logs -f polaris-backend-java

리소스 사용량
docker stats
문제 발생 시 바로 원인 파악 가능.

4. 충분한 성능

동시 접속자 수십~수백 명 처리 가능
Spring Boot의 Tomcat: 기본 200개 쓰레드
AI Agent의 Uvicorn: 비동기 처리로 높은 처리량

단점 (Kubernetes 대비)

1. 수동 스케일링

트래픽 급증 시 수동으로 인스턴스를 추가해야 함.
Kubernetes는 자동
kubectl scale deployment myapp --replicas=10

우리는 수동
docker run -d --name myapp-2 myimage
docker run -d --name myapp-3 myimage

2. 무중단 배포 불가

docker stop myapp # ← 이 순간 서비스 중단!
docker run -d --name myapp myimage
Kubernetes의 롤링 업데이트는 중단 없이 배포 가능. 해결 방법: Blue-Green 배포
Green (새 버전) 실행
docker run -d --name myapp-green myimage

Nginx 설정 변경: myapp → myapp-green

Blue (구 버전) 중지
docker stop myapp

3. 자가 치유 없음

컨테이너가 죽으면 수동으로 재시작해야 함.
docker run -d --restart unless-stopped myapp # ← 이것으로 부분 해결
Kubernetes는 자동으로 Pod를 재시작함.

실제로 겪은 배포 문제들

1. 문제: MailConfig 때문에 ApplicationContext 로드 실패

증상:
Failed to load ApplicationContext
Caused by: Could not resolve placeholder 'spring.mail.host'
원인: MailConfig가 @Configuration으로 무조건 로드되는데, 메일 환경 변수가 없으면 실패. 해결:
@Configuration
@ConditionalOnProperty(name = "spring.mail.host") // ← 추가
public class MailConfig {
// ...
}

2. 문제: AWS S3 설정 때문에 시작 실패

증상:
Could not resolve placeholder 'AWS_ACCESS_KEY'
원인: GCP로 전환했는데 AWS S3 설정이 남아있었음. 해결:
S3Config.java 삭제
rm src/main/java/com/skax/physicalrisk/config/S3Config.java

application.yml에서 AWS 설정 제거

3. 문제: PostgreSQL 테이블이 없어서 실패

증상:
Schema-validation: missing table [analysis_jobs]
원인: ddl-auto: validate로 설정되어 테이블이 없으면 실패. 해결:
application-prod.yml
jpa:
hibernate:
ddl-auto: update # validate → update 변경

4. 문제: Docker 컨테이너끼리 통신 안 됨

증상:
curl: (6) Could not resolve host: polaris-backend-fastapi
원인: 컨테이너들이 다른 네트워크에 있었음. 해결:
공통 네트워크 생성
docker network create web

모든 컨테이너를 web 네트워크에 연결
docker run -d --network web --name polaris-backend-java ...
docker run -d --network web --name polaris-backend-fastapi ...

언제 Kubernetes로 전환해야 할까?

다음 상황이 오면 Kubernetes를 고려해야 함:

1. 트래픽 폭증

동시 접속자: 수백 명 → 수천~수만 명
→ 오토스케일링 필수

2. 마이크로서비스 확장

서비스 개수: 4개 → 10개 이상
→ 서비스 디스커버리, 로드 밸런싱 필요

3. 고가용성 요구

SLA: 99% → 99.9% 이상
→ 멀티 리전 배포, 자동 장애 복구 필요

4. 멀티 클라우드

GCP + AWS + Azure 동시 사용
→ Kubernetes는 클라우드 벤더 중립적

정리

항목 | Docker + Nginx | Kubernetes
학습 곡선 낮음 ⭐ 높음 ⭐⭐⭐⭐⭐
초기 비용 낮음 ($25/월) 높음 ($150/월~)
배포 복잡도 낮음 높음
확장성 제한적 무제한
무중단 배포 수동 (Blue-Green) 자동 (Rolling)
자가 치유 제한적 (restart 옵션) 자동 (Pod 재시작)
모니터링 수동 (docker stats) 자동 (Prometheus)
적합한 규모 소규모~중규모 중규모~대규모

우리의 선택: Docker + Nginx
✅ 3개월 프로젝트 기간
✅ 4개 서비스 (프론트엔드, 백엔드, AI Agent, ModelOps)
✅ 중소규모 트래픽 (동시 접속자 수십~수백 명)
✅ 제한된 예산 ($75 vs $450)
✅ 빠른 배포와 디버깅

배운 점

1. 적정 기술 선택의 중요성

"최신 기술 = 좋은 기술" 이 아니다. 프로젝트 규모와 요구사항에 맞는 기술을 선택하는 게 중요하다.

2. 인프라는 단순할수록 좋다

복잡한 인프라는 디버깅도 어렵고, 팀원 온보딩도 어렵다. Docker + Nginx는 누구나 이해할 수 있다.

3. 비용 최적화

스타트업이나 소규모 프로젝트에서는 비용이 중요하다. Kubernetes로 $375를 절약한 건 큰 성과다.

4. 확장 가능한 설계

지금은 Docker + Nginx지만, 나중에 Kubernetes로 전환할 수 있도록 설계했다:
12 Factor App 원칙 준수
환경 변수로 설정 관리
컨테이너 기반 배포
상태를 저장하지 않는 Stateless 서비스


"기술은 목적이 아니라 수단이다. 과하지도 부족하지도 않은, 딱 맞는 기술을 선택하자."

profile
안녕하세요

0개의 댓글