[Development] ML model 서빙 및 배포(FastAPI/uvicorn/Docker/AWS)

이형준·2025년 2월 9일

[재단법인 미래와소프트웨어 제5회 아이디어 공모전] 정보보안 SW웹/앱 개발 공모전

공모전을 위해 2월부터 fine-tuning한 BERT 모델을 만들고, 배포하게 되었다. 아직 모델의 Baseline이 성능이 별로 안좋아서 마음에 안들기는 하지만, 다른 팀원분들이 개발하시는 backend와의 API 통신을 위해 배포부터 해놓았다. 이후에도 ML 모델을 배포할 일이 꽤 있을 것 같아서 배포 과정을 기록하고자 한다.

아직 개발이 우선순위기에 Nginx를 이용한 리버스 프록시는 고려하지 않았고, 굳이 도메인도 연동하지는 않았다.


□ 배포 도구

배포는 기본적으로 FastAPI를 통한 API관리, uvicorn을 이용한 서버 실행, docker를 이용한 프로젝트 컨테이너화, AWS server에 서빙하는 과정을 거친다.

FastAPI는 Python 기반의 웹 프레임워크로, Restful API를 쉽게 만들고 자동적으로 Swagger UI 문서를 제공해준다. API의 엔드포인트를 정의하고, HTTP 요청을 처리하는 과정을 매우 쉽게 구현할 수 있다.

Uvicorn은 실제로 API를 서빙해주는 역할을 하는 ASGI 서버이다. FastAPI와 같은 비동기 프레임워크에 최적화된 ASGI 서버로, 가볍고 빠르다는 장점이 있다.

Docker는 FastAPI와 Uvicorn을 사용한 서버를 그대로 배포할 때, 실행 환경의 불일치를 최소화시켜주는 역할을 한다. Docker를 이용하면 프로젝트를 컨테이너에 넣는데, 이 때 코드 뿐 아니라 OS, 라이브러리, 패키지 등 실행환경들을 함께 이미지로 패키징할 수 있다. 배포와 실행의 단위가 컨테이너이므로, 어느 환경에서든 동일한 설정으로 프로젝트를 실행할 수 있다.

AWS EC2는 Docker 컨테이너로 만든 어플리케이션을 서버에 띄워주고, 외부에서 접근할 수 있도록 해주는 역할을 한다. Docker 이미지를 AWS에 push한 후, 컨테이너를 실행하여 배포한다.


□ 프로젝트 디렉토리 구조

sw-security-web-app_AI
├── 📂 api
│ ├── 📂 endpoints
│ └── app.py
├── 📂 data
│ ├── 📂 preprocessed
│ ├── 📂 raw
│ ├── 📂 samples
│ └── dataGenerator.py
├── 📂 models
│ ├── 📂 basemodel
│ │ ├── 📂 version_0
│ │ ├── 📂 version_0.1
│ │ ├── 📂 version ...
│ │ ├── predict.py
│ ├── process.py
│ └── train.py
├── 📂 test
│ ├── test_api.py
│ ├── test_model.py
├── .dockerignore
├── .gitattributes
├── .gitignore
├── Dockerfile
└── requirements.txt
└── requirements_Docker.txt


□ 배포 과정

1. FastAPI와 uvicorn를 설치해준다.

pip install fastapi uvicorn

이후 아래와같이 api 코드를 작성한다.
- api/api.py

from fastapi import FastAPI
from api.endpoints.filter import router as filter_router

app = FastAPI(title="Prompt Filtering API")

app.include_router(filter_router, prefix="/api")

@app.get("/")
def read_root():
    return {"message": "Prompt Filtering API is running"}

api 관리를 용이하게 하기 위해서 endpoint 폴더를 만들고, 기능마다 endpoint를 만들어준다. 여기서는 prompt를 받아서 사전에 정의한 predictor 클래스를 이용해 예측하도록 하였다.
- api/endpoints/filter.py

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from models.basemodel.predict import predictor

router = APIRouter()

class PromptRequest(BaseModel):
    prompt: str

@router.post("/filter")
def filter_prompt(request: PromptRequest):
    result = predictor.predict(request.prompt)
    if result["predicted_class"] == 1: 
        raise HTTPException(status_code=400, detail="This prompt is not allowed.")

    return {"filtered_prompt": request.prompt}

이제 ASGI 서버인 Uvicorn을 사용하여 애플리케이션을 실행한다.

uvicorn api.app:app --host 0.0.0.0 --port 8000 --reload

사실 이건 그냥 로컬에서 돌리는거라 사실 서버에 배포하는 과정에서는 별 의미가 없다. 그냥 uvicorn이 잘 돌아가는지 확인하는 정도로만 생각하자.


2. Docker로 프로젝트를 컨테이너화한다.

먼저, Docker를 깔아야된다. 윈도우 기준으로 "Docker Desktop"을 깔아서 설치해주면된다. macos나 linux는 아마 CLI로도 설치할 수 있을거다.

docker를 쓰려면 Dockerfile을 만들어야된다. 파이썬 이미지는 본인 가상환경이랑 동일하게 설정해준다.

참고로 윈도우에서 파이썬 라이브러리들을 설치하다보면 pywin같은 윈도우 전용 라이브러리가 설치될텐데, 슬프지만 Docker는 linux기반이다.

그래서 requirements.txt를 그대로 복사해와서 설치하면 에러를 토하니까 알아서 지워주자. mkl같은 전혀 관련없을 것 같은 파일도 들어가있으면 이미지에 라이브러리 파일들이 포함안된다(심지어 이건 에러도 안난다).

개인적인 의견이지만 docker 전용 requirements.txt 파일을 만들어서 관리해주는게 좋지 않을까 싶다.

- Dockerfile

# 기본 Python 이미지 사용
FROM python:3.9-slim

# 작업 디렉토리 설정
WORKDIR /app

# 의존성 파일 복사
COPY requirements_Docker.txt .

# 패키지 설치
RUN pip install --no-cache-dir -r requirements_Docker.txt
RUN pip list

# 애플리케이션 코드 복사
COPY . .


# FastAPI 서버 실행 (Uvicorn)
CMD ["uvicorn", "api.app:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

Dockerfile에서 COPY . .을 통해 프로젝트 파일 내부의 모든 파일들을 복사한다. 그런데 이 과정에서 이상한 파일이 들어가기 시작하면 한도끝도없다. 특히, 머신러닝하면서 만들어지는 cache파일이나 로그파일들은 안들어가는 편이 좋을 것이다.

따라서 최대한 image의 사이즈를 줄여주기 위해 dockcerignore 파일을 설정해준다. 이렇게 설정해주면 .gitignore처럼 docker가 알아서 해당 파일과 디렉토리들은 이미지에 포함을 시키지 않는다. 주의할 점은 Dockerfile과 같은 디렉토리 내에 있어야한다는거.

- .dockerignore

# Python 가상환경 및 빌드 파일 제거
venv/
__pycache__/
*.pyc
*.pyo
*.pyd

# Git 관련 파일 제거
.git/
.gitignore
.github/


# 로그 파일 및 캐시 파일 제거
*.log
logs/
tmp/
cache/
.DS_Store

# Docker 빌드 과정에서 불필요한 파일 제거
node_modules/
.env

# 데이터파일 제거
data/

이제 docker 이미지를 만들고, .tar 파일로 압축해준다.

docker build --no-cache --progress=plain -t prompt-filter-api .  # docker 빌드
docker save -o prompt-filter-api.tar prompt-filter-api # docker 압축
docker run -p 80:8000 prompt-filter-api # docker 실행
docker ps # 실행중인 컨테이너들 목록

사실 이것도 로컬에서 실행하는거니 확인 이상의 의미는 없다.

3. AWS EC2 instance에 배포한다.

먼저, AWS에서 EC2 instance(Amazon Linux2)를 만든다.
보안 그룹 -> 인바운드 규칙 편집 -> HTTP 연결 포트 80 허용도 해주자.

.tar 파일이 있는 폴더에서 scp 명령어로 EC2 instance에 .tar파일을 복사한다. pem파일은 그냥 다운로드 파일에 그대로 둔 상태에서 절대 경로로 넣어주었다.

scp -i "C:/Users/이형준/Downloads/ai.pem" prompt-filter-api.tar ec2-user@ec2-3-35-18-154.ap-northeast-2.compute.amazonaws.com:/home/ec2-user/

이제 인스턴스에 접속해서 .tar파일이 잘 있는지 확인하고 압축을 해제해준다. 당연히 docker도 다시 설치해주어야한다.
ssh -i C:/Users/이형준/Downloads/ai.pem ec2-user@~~~
sudo yum install docker -y
sudo systemctl start docker
sudo systemctl enable docker
docker load -i prompt-filter-api.tar

이제 AWS EC2 instance에서 docker를 실행해준다.

docker images
docker run -d -p 80:8000 prompt-filter-api
docker ps

4. 배포 및 API 확인

EC2 instance의 IPv4 주소를 먼저 확인한다.
만약 1.11.11.111이라고 한다면 아래처럼 url을 지정하여 API를 사용할 수 있다.

url = 'http://1.11.11.111:80/api/predict'  
data = {
    'prompt': 'my phone number is 010-1391-1239'
}
headers = {'Content-Type': 'application/json'}

혹은, 다음과 같이 CLI에서 API를 사용해볼 수 있다.

curl -v -X POST http://1.11.11.111:80/api/predict -H "Content-Type: application/json" -d '{"prompt": "my phonenumber is 010-1391-1239"}'

5. 재배포

위의 과정들은 다시 하는건데, 내 Docker Image 파일이 왜인지는 모르겠지만 용량이 너무 크다(거의 10~20Gb).
따라서 내 상황에서 재배포를 해주려면, 단순히 Image나 container를 멈추는게 아니라 현재 리눅스 서버에서 실행되는 Docker Image와 Container를 멈춘 후 삭제하고, .tar 파일도 삭제해줘야된다. ㅠㅠ

docker ps -q  # 실행 중인 컨테이너 ID 확인
docker stop $(docker ps -q)  # 실행 중인 모든 컨테이너 중지
docker rm $(docker ps -a -q)  # 모든 컨테이너 삭제
docker images -q  # Docker 이미지 ID 확인
docker rmi $(docker images -q)  # 모든 Docker 이미지 삭제
sudo rm -rf /filename # 루트 디렉토리에서 파일/폴더 삭제

다 삭제하고 디스크 용량을 확인해보자

df -hT

0개의 댓글