개발/오늘 배운 지식

[FastAPI/Python] FastAPI 프로젝트 배포하기

Woogie2 2022. 8. 18. 16:51
반응형

Chapter 10: FastAPI 프로젝트 배포하기

이 단원에서는 FastAPI 프로젝트를 배포하는 Best Practice에 대해 배운다. 먼저 환경 변수를 사용해서 configuration을 진행하고, dependency들을 pip로 잘 관리될 수 있게 한다.

그 후 세 가지 방법으로 배포를 진행해 본다.

  • 서버리스 클라우드 플랫폼 이용해서 배포
  • 도커 컨테이너를 이용한 배포
  • 리눅스 서버를 이용한 배포

또 다른 주제들은 아래와 같다

  • 환경변수를 설정하고 사용하는 방법
  • 파이썬 종속성 관리하기

환경변수를 설정하고 사용하기

배포에 대한 방법을 배우기 전에, 먼저 우리가 구현한 앱을 신뢰할 수 있고, 빠르고, 안전하게 배포할 수 있도록 만들어야 한다. 그 과정에 있어서 핵심적인 것이 configuration 관련 변수를 다루는 것이다. 데이터베이스 URL, 외부 API 키, 디버그 flag 등이 있다. 그리고 이 변수들을 하드코딩하지 않도록 하는 것이 필요하다. 그 이유는 뭘까?

먼저, production level로 가거나 각 Local 컴퓨터마다 환경변수들이 다르다. 둘째로, 이 값들은 매우 민감한 정보기 때문에 코드에 적기에는 매우 위험하다. 이는 보안 이슈를 초래할 것이다.

이 위험을 해결하는 것이 바로 환경 변수(environment variables)이다. 이 값은 하나의 프로그램 내에 저장되는 것이 아니라 전체 시스템(로컬 OS)에 저장된다.

UNIX의 경우 아래와 같이 테스트해 볼 수 있다.

$ export MY_ENVIRONMENT_VARIABLE="Hello" # Set a temporary variable on the system
$ python
>>> import os
>>> os.getenv("MY_ENVIRONMENT_VARIABLE")  # Get it in Python
'Hello'

파이썬 소스코드에서 우리는 시스템으로부터 동적으로 환경 변수를 가져올 수 있다. 하지만, 환경 변수는 여전히 보안상으로 취약하다. 로그 파일이나 에러 스택에서 이 값들이 노출될 수도 있다.

이 문제점을 Pydantic의 기능(setting management)으로 해결해보자. 이 기능은 다른 데이터 모델을 만들때 처럼, 환경 변수와 각종 설정 관련 변수를 데이터 모델로 정의하고 다룰 수 있게 해 준다. 그리고 시스템으로부터 환경 변수들을 알아서 받아온다.

이번 장의 예제코드는 project 폴더로 경로를 바꾼 다음, 명령어를 실행하도록 한다(cd chapter10/project).

project 내부를 확인해보면 setting.py를 찾을 수 있다.

from pydantic import BaseSettings

class Settings(BaseSettings):
    debug: bool = False
    environment: str
    database_url: str

이렇게 모델을 선언할 수 있다. 기본값 설정이 가능하고, 환경변수 누락 시 오류 발생 등을 알아서 처리해주니 편리하다. 그리고 다른 Pydantic 모델처럼 다룰 수 있기에 편리하다.

실제로 이 값들을 사용하려면 app.py로 클래스를 불러오고 인스턴스를 생성하면 된다.

from app.settings import Settings

settings = Settings()
app = FastAPI()

이 상황에서 아래의 명령어를 실행하면, 환경변수를 설정하지 않았기 때문에 오류가 발생한다.

$ uvicorn app.app:app

다음과 같은 명령어로 환경변수를 임시로 설정해보자.

$ export DEBUG="true" ENVIRONMENT="development" DATABASE_URL="sqlite://chapter10_project.db"

그리고 uvicorn 명령어를 실행하면 정상적으로 동작할 것이다.

Untitled

우리가 @app.on_event(”startup”)에서 debug 설정이 있는 경우 각 설정을 프린트하도록 했기 때문에 그림처럼 출력된다.

.env 파일 사용하기

로컬에서 개발할 때, 환경 변수를 수동으로 설정하는 것은 성가신 일이다. 특히나 여러 프로젝트를 관리한다면 더욱 귀찮아진다. 이 문제를 해결하기 위해 Pydantic에서는 .env 파일에서 값들을 읽어올 수 있게 한다. 이 기능을 사용하려면 python-dotenv를 설치해야 한다. 그리고 위에 Settings 클래스를 정의한 부분에서 sub-class로 Config 클래스를 추가로 선언하고 env_file 필드를 추가한다.

from pydantic import BaseSettings

class Settings(BaseSettings):
    debug: bool = False
    environment: str
    database_url: str

    class Config:
        env_file = ".env"

이제 .env 파일을 생성해서 환경변수 값을 KEY=value 형태로 저장하면 손쉽게 환경변수를 관리할 수 있다. 이 파일은 .gitignore에 추가해서 민감한 값들이 외부로 노출되지 않도록 한다(개발 편의를 위한 것이므로).

파이썬 종속성 관리하기

이 책에서 여러 단원을 거치며 많은 라이브러리를 설치하였다. 개발을 마친 후, 새로운 배포 환경에서 이 파이썬 라이브러리들이 정상적으로 설치되고 버전이 관리되어야 한다.

대부분의 파이썬 프로젝트는 requirements.txt 파일로 여러 라이브러리의 버전을 기록해둔다. 이 파일은 프로젝트 루트에 위치시키는 것이 일반적이다.

개발을 진행하던 파이썬 환경(주로 가상 환경)의 라이브러리 버전을 pip freeze로 쉽게 파악하고, 이 명령어의 출력 내용을 requirements.txt에 바로 붙여 넣으면 돼서 편리하다. 하지만 이 명령어의 문제점은 꼭 필요한 라이브러리만 골라서 파악할 수는 없다는 점이다. 이 목록에는 설치하지 않아도 되는 라이브러리들도 포함되어 있다.

따라서, 추천하는 방법은 직접 라이브러리를 설치, 삭제할 때마다 라이브러리 이름과 버전을 requirements 파일에 추가/삭제하는 것이다.

다른 python 종속성 관리 툴에 대하여

파이썬 생태계에는 Pipenv, Conda 등의 다른 가상 환경 및 패키지 관리 툴이 있다. 이들은 pip를 대체할만한 뛰어난 도구이지만, 클라우드 플랫폼들이 아직 전통적인 requirements.txt파일을 요구한다는 점을 알아둬야 한다. 그러므로, 최근에 등장한 패키지 관리 툴은 우리에게 적절한 선택지는 아니다.

이렇게 기록한 라이브러리들은

$ pip install -r requirements.txt

명령어로 한 번에 설치할 수 있다.

Gunicorn을 배포용 서버 프로세스로 사용하기

이 블로그에서 글을 작성하진 않았지만, 책의 2단원에서 WSGI, ASGI 프로토콜에 대해서 설명했었다. 각 프로토콜은 파이썬으로 웹 서버를 만들 때의 표준 규약과 데이터 구조에 대해 정의한다. 전통적인 파이썬 웹 프레임워크인 장고나 플라스크는 WSGI 프로토콜을 따른다. ASGI는 최근에 탄생했으며, WSGI의 “spiritual successor” 역할을 하고 있다. 그리고 비동기적으로 동작하는 서버에 대한 프로토콜이다. 이 ASGI는 FastAPI와 Starlette에 핵심이라고 할 수 있다.

챕터 3에서 처음 개발을 시작할 때, 우리는 로컬 환경에서 ***Uvicorn*** 을 사용했다. 이것은 HTTP 요청을 받아오고, ASGI 프로토콜에 맞게 변환하고, FastAPI에 전달한다. 그러면 FastAPI는 ASGI 프로토콜에 맞는 응답 객체를 Uvicorn에게 반환하고, Uvicorn은 이 응답 객체를 HTTP 응답 객체로 변환하는 역할을 한다.

WSGI에서는 가장 많이 쓰이는 서버 프로세스는 Gunicorn이다. 이것은 장고와 플라스크가 사용될 때, 비슷한 역할을 한다. 이 서버 프로세스를 이야기하는 이유는 Uvicorn에 비해 실제 프로덕션 수준에서 사용할 할 때, robust하고 reliable하기 때문이다. 그러나 Gunicorn은 WSGI 프로토콜에 맞게 설계되었는데, 어떻게 사용하는 것일까?

사실, 두 가지 모두 사용할 것이다.

  • Gunicorn: Robust 한 process manager
  • Uvicorn: ASGI application(FastAPI) 실행 용도

이 방식은 실제 Uvicorn 문서에서 권장하는 방법이다.

이제 gunicorn을 설치하고, requiremets 문서에도 추가해주자.

$ gunicorn -w 4 -k uvicorn.workers.UvicornWorker app.app:app

이 명령어로 FastAPI 앱을 gunicorn에서 실행할 수 있다. 하지만 명령어를 입력할 때, uvicorn의 worker를 사용하도록 추가로 설정을 해주어야 ASGI 프로토콜에 맞게 동작한다. -w 옵션은 서버의 worker 개수를 설정할 때 사용한다. 여기서는 4개의 worker를 설정했다. 그러면 Gunicorn이 4개의 워커를 로드밸런싱하며 실행한다. 이 로드밸런싱 기능이 Gunicorn을 쓰는 이유 중 하나이다.

Serverless 플랫폼을 이용해서 배포하기

서버리스 플랫폼은 최근 많은 인기를 얻고 있는데, Google App Engine, Herokum, Azure App Service가 유명하다. 각 플랫폼마다 비슷한 절차로 배포가 진행된다.

보통, 서버리스 플랫폼은 github 레포 형태로 소스코드를 제공받길 원한다. 직접 플랫폼 서버에 업로드하거나, 서버가 깃헙에서 받아온다. 일반적인 과정을 정리해보자.

  1. 클라우드 플랫폼을 결정하고 계정을 만든다. 대부분의 클라우드 플랫폼은 무료 크레딧을 제공하므로 활용하자!
  2. CLI 툴을 설치한다.
  3. 앱 설정을 진행한다. yaml 파일과 같은 것으로 설정하는 것이 일반적이다.
  4. 환경 변수 설정하기.
  5. 앱 배포하기. 대부분 CLI 툴 명령어로 배포를 진행한다

DB 서버 추가하기

  • Google Cloud SQL, Azure Database for PostgreSQL, Amazon RDS, Heroku Postgres 등의 서비스를 이용하면 비교적 편리하게 추가할 수 있다.

서버리스 플랫폼을 통해서 배포하는 것이 가장 손쉬운 방법이지만, 커스터마이징이 필요하거나 추가적인 통제가 필요한 경우 도커 컨테이너를 활용해서 배포하는 것이 좋다.

도커를 활용하여 FastAPI 앱 배포하기

도커는 컨테이너화(containerization)에 자주 이용되는 소프트웨어다. 컨테이너(Container)는 small, self-contained system이다. 각 컨테이너는 웹 서버, DB 엔진, 데이터 처리 앱 등의 단일 애플리케이션을 실행하기 위한 파일과 설정을 가지고 있다. 컨테이너를 쓰는 주된 목적은, 이 앱들을 dependency와 버전 충돌 등의 걱정을 하지 않고 실행하는 것이다.

Dockerfile에 명령어를 입력해두면 작은 시스템을 명령어에 맞게 생성한다(도커의 portable + reproducible한 특성 덕분에). 이 명령어들은 **build** 과정에서 실행되고, 결과물은 **Docker image**라고 부른다. 이 도커 이미지는 즉시 사용할 수 있는 시스템을 포함한 하나의 패키지다. 이 이미지를 registries로 인터넷에 쉽게 공유할 수 있다.

도커를 사용해서 복잡했던 개발 환경 설정이 쉬워졌다. 각자의 로컬 컴퓨터에 직접 설치하는 것이 아니라 컨테이너 내에서 따로 설치하기 때문에 버전이 다른 여러 프로젝트를 동시에 관리할 수 있는 것이다.

그러나 도커는 로컬에서 개발하는 용도만이 아니라 배포를 할 때도 유용하다. 빌드 과정이 reproducible하기 때문에 로컬과 배포되는 환경이 동일하다고 보장할 수 있다.

도커 파일 작성하기

우리가 만든 앱을 실행할 때, 도커 컨테이너를 만들고 도커 이미지를 빌드해서 실행해야 한다. 그 이미지를 만들 때, base image(일반적인 Linux인 Debian이나 Ubuntu가 설치된 도커 이미지)로부터 시작해서, 우리가 만든 소스코드를 이미지 내부로 복사해온다. 그리고 Unix 커맨드를 실행(도커 파일을 통해서)시켜서 우리가 원하는 앱이 동작하는 시스템을 구성하면 되는 것이다.

우리 케이스의 경우 FastAPI 제작자가 base Docker image(FastAPI 실행을 위한 툴을 미리 설치해둔)를 만들어서 제공한다. 따라서 이 이미지를 사용하면 편리하다.

그러기 위해서는 먼저 프로젝트의 루트 경로에 Dockerfile을 생성한다.

FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
ENV APP_MODULE app.app:app
COPY requirements.txt /app
RUN pip install --upgrade pip && \
    pip install -r /app/requirements.txt
COPY ./ /app

여러 버전의 도커 이미지가 제공된다. 자세한 내용은 아래의 깃허브 레포에서 확인 가능하다. 레포에 들어가서 확인해보니 쿠버네티스와 같은 툴을 사용하는 경우에는 베이스 이미지가 필요하지 않고, 처음부터 이미지를 새로 만드는 것이 좋다고 한다. 관련된 링크를 참조해서 이미지를 만들자.

이제, ENV 명령어를 통해서 APP_MODULE 환경 변수를 설정한다. 도커 이미지가 빌드될 때 이 환경 변수가 설정된다(혹은 런타임에 설정되도록 할 수도 있음). 이 변수는 FastAPI 앱의 경로를 가리키고 있어야 한다. 나머지 설정 가능한 환경 변수는 FastAPI 베이스 이미지 레포의 리드미를 읽어보고 파악하자.

이제 COPY 문으로 로컬 시스템에서 이미지 내부로 파일을 복사한다. 여기서는 requirements.txt 만을 복사했다(이유는 곧 설명할 것임). /app 경로가 베이스 이미지가 구동되는 루트 경로이다.

이제 RUN 구문으로 Unix 명령어를 실행한다. pip을 이용해서 우리가 설치해야 하는 라이브러리를 설치한다.

마지막으로, 우리의 나머지 소스코드를 /app 폴더로 복사한다. 이제 requirements 파일만 따로 복사했던 이유를 알아보자. 중요한 점은 도커 이미지가 레이어들로 이루어져 있다는 점이다. 각 레이어들은 도커 명령어마다 새로 생성된다. 성능을 향상하기 위해서 도커는 이전에 사용했던 레이어를 재사용한다. 그러므로 이전 빌드와 차이점이 없으면 메모리에 저장된 미리 빌드된 레이어를 다시 사용한다.

따라서 우리가 requirements 파일은 건들지 않고 소스코드를 일부 수정했다면, 빌드를 새로 진행하지 않고 소스코드만 새로 복사하면 된다. 그리고 빌드 과정을 진행하지 않기 때문에 더욱 빠른 실행이 가능해진다(몇 분의 빌드 시간이 몇 초의 복사 시간으로 줄어듦).

대부분 도커 파일은 CMD 명령어로 끝난다. 이 명령어는 컨테이너가 동작을 시작한 다음 수행되어야 한다. 이 경우 Gunicorn을 추가했던 단원에서 실행했던 명령어를 CMD 문 뒤에 넣으면 된다. 하지만 지금의 경우 FastAPI에서 제공하는 베이스 이미지를 사용했기 때문에, 추가하지 않아도 된다.

도커 이미지 빌드하기

이제 도커 파일 작성을 완료했으니 이미지를 빌드해보자.

$ docker build -t fastapi-app  .

Dot(.)은 이미지를 빌드할 root context를 말한다. -t 옵션은 이미지에 사람이 이해할 수 있는 이름을 부여하는 것이다.

이 명령어를 실행하면 도커 허브에서 베이스 이미지를 받아오고, 빌드가 된다. 이제 빌드된 이미지를 구동시켜보자.

도커 이미지를 로컬에서 동작시키기

$ docker run -p 8000:80 -e ENVIRONMENT=production -e DATABASE_URL=sqlite://./app.db fastapi-app

이 명령어를 실행시키면, Gunicorn으로 FastAPI 앱이 실행된다. 여기서 명령어에 입력된 옵션들을 알아보자.

  • -p: 로컬 머신에서 접속할 수 있는 포트를 할당한다. 기본값으로 되어있다면, 우리가 로컬 머신에서 도커 컨테이너로 접근이 불가능하다. 이 옵션을 통해서 지정해두면 localhost:8000으로 접근이 가능하다. 로컬 머신에서는 8000번 포트, FastAPI를 담은 컨테이너 내부에서는 80번 포트에 연결된다는 뜻이다.
  • -e: 환경 변수 설정에 사용된다. 이 flag를 추가해서 런타임에 환경변수로 설정할 수 있다. 여기서는 DB URL을 테스트용으로 sqllite에 연결했다.
  • docker run 옵션들에 대한 상세한 정보

도커 이미지 배포하기

각 클라우드 플랫폼마다 이 컨테이너 이미지를 자동 배포할 수 있는 서비스를 제공한다.

  • Google Cloud Run
  • Amazon Elastic Container Service
  • Microsoft Azure Container Instances

등이 있다.

보통 우리가 해야 할 것은 우리가 빌드한 이미지를 레지스트리에 업로드하는 것이다. 기본적으로 도커는 도커 허브에서 이미지를 push & pull 한다. 다른 서비스를 이용해서 배포할 때는 각 플랫폼의 레지스트리에 업로드를 따로 해주어야 한다.

  • Google Artifact Registry
  • Amazon ECR
  • Microsoft Azure Container Registry

이런 서비스를 상황에 맞게 선택한다. 아래의 명령어들은 private 레지스트리에 업로드한 이미지를 어떻게 로컬 도커 CLI에서 업로드하는지 보여준다.

$ docker tag fastapi-app aws_account_id.dkr.ecr.region.amazonaws.com/fastapi-app

이렇게 빌드한 이미지를 태그 한다.

$ docker push fastapi-app aws_account_id.dkr.ecr.region.amazonaws.com/fastapi-app

태그 한 이미지를 (클라우드 플랫폼의) 레지스트리에 푸시한다. 여기까지의 내용이 가장 쉽고 효율적으로 도커 이미지를 배포하는 방법이었다.

FastAPI 앱을 전통적인 서버에 배포하기

보안과 같은 여러 이유로 서버리스 플랫폼을 통해 배포할 수 없는 상황이 생길 수 있다. 이때, 몇 가지 기본적인 내용을 알아두면 배포에 도움이 될 것이다.

일단, 이 섹션에서는 리눅스 서버에 배포하는 것을 전제로 진행한다.

  1. 먼저, 서버에 최신 버전의 파이썬이 최신 버전으로 유지되고 있는지, 그리고 개발할 때 사용한 버전과 일치하는지 확인한다(책에서는 파이썬 버전 설정을 pyenv로 했음).
  2. 서버에서 깃허브 레포를 pull 해서 최신 버전의 소스코드를 유지한다.
  3. python 가상 환경을 설정한 다음, pip로 필요한 라이브러리들을 설치하자(requirements.txt 이용).
  4. Gunicorn 실행하고 FastAPI 앱 실행하기
    1. 그러나 몇 가지 과정이 필히 추가되어야 함
  5. Process manager를 사용해서 Gunicorn 프로세스가 항상 구동되고 있도록 만든다. Supervisor 사용을 추천함. Gunicorn 문서에서 가이드라인을 제공한다.
  6. Gunicorn을 HTTP proxy 뒤로 숨기기. 즉, HTTP proxy가 먼저 요청을 받아서 Gunicorn에게 전달하는 방식으로 세팅해서 프록시가 SSL 연결, 퍼포먼스 로드 밸런싱, static 파일 제공 등을 하도록 한다. Gunicorn 문서에서는 Nginx 사용을 추천함.

추가적으로 서버의 보안이 취약하지 않은지도 확인해주어야 한다. 관련 문서 참고

요약

이 단원에서는 우리가 만든 FastAPI 앱을 배포하는 좋은 예시들을 알아보았다.

  • .env 파일 사용해서 환경 변수 설정하는 것
  • requirements.txt 파일로 파이썬 dependency 관리하는 것
  • 서버리스 플랫폼으로 배포하는 것
  • 도커
    • 서버리스 플랫폼에서 도커 이미지 배포하기
    • 전통적인 리눅스 서버에 도커 이미지 배포하기

등을 배웠다.

반응형