FastAPI 프로젝트 배포:Dockerzie

지진우·2023년 12월 12일
0

1. Introduction

이 시리즈에선 제가 Jobdam이란 프로젝트에서 서버를 실제로 어떻게 배포했는지 살펴보겠습니다.

프로젝트를 배포하는 한 가지 방법은 Docker를 사용하여 컨테이너 이미지를 빌드하는 것입니다. 이 글에서는 Docker의 기본 개념과 함께 FastAPI 프로젝트를 어떻게 컨테이너 이미지로 만드는지 알아봅니다.

2. Docker(도커)란?

도커는 소프트웨어를 컨테이너(Container) 라는 표준화된 유닛으로 패키징하여 애플리케이션을 구축하고 배포할 수 있게 해주는 오픈소스 프로젝트입니다.

간단하게 말하면, 애플리케이션의 코드와 설치된 라이브러리 등을 컨테이너라는 공간에 담아서 배포하기 쉽게 도와주는 플랫폼입니다.

그렇다면 컨테이너란 무엇일까요

2-1. 컨테이너(Container)

흔히 말하는 소프트웨어는 OS와 라이브러리에 의존성을 가집니다. 만약 서로 다른 OS나 버전이 다른 라이브러리의 소프트웨어를 실행하고자 할 때 오류를 가져올 수 있고 이를 제어하기가 어려워집니다.

컨테이너는 소프트웨어(애플리케이션)를 실행할 때 독립적인 환경을 제공해주는 운영체제 수준의 격리 기술을 말합니다.

설명을 위해 가상 머신과 컨테이너를 비교해보겠습니다. 아래 사진과 같이 가상 머신은 OS 레벨을 포함한 전체를 가상화하는 가상머신과 달리 격리된 프로세스로 실행되는 컨테이너는 가상머신에 비해 매우 가볍습니다.

쉽게 얘기하면 window에서 개발하는 사람과 mac에서 개발하는 사람은 OS가 다릅니다. 또한 사용하는 언어의 버전이나 라이브러리 버전 등이 다를 수 있는 것을 방지하고자 컨테이너를 사용하여 동일한 OS단에서 코드와 라이브러리 버전의 통일성을 만드는 것입니다.

여기서 컨테이너 실행에 필요한 프로그램과 소스코드, 라이브러리 등을 포함시킨 것 파일을 Docker image(도커 이미지)라고 합니다.

2-2. 도커 이미지(Docker image)

도커 이미지는 서비스 운영에 필요한 프로그램, 소스코드, 라이브러리 등을 포함한 파일입니다. 즉, 특정 프로세스가 실행되기 위한(여기선 컨테이너 실행) 모든 파일과 환경을 지닌 것으로 더 이상의 의존성 파일을 컴파일하거나 라이브러리를 설치할 필요가 없는 상태의 파일을 의미합니다.

이미지는 읽기 전용(read-only)으로 스냅샷이라고도 합니다. 따라서 이미지는 실행할 수 없는 정적 이미지(불변성) 입니다. 즉, 이미지는 빌드 타임 구조로 이루어져 있고 컨테이너는 런타임 구조로 실행 중인 이미지를 나타냅니다.(예를 들면 객체 지향 언어에서의 클래스와 해당 클래스 객체의 차이인 것 같습니다?)

2-3. 레이어(Layer)

도커 이미지는 컨테이너를 생성하기 위한 모든 정보를 가지고 있기 때문에 보통 수백MB ~ 수GB가 넘는다. 그런데 기존 이미지에서 작은 변경사항이 생겨 도커 파일에 코드 몇 줄을 추가해 다시 이미지를 만들고 다시 그 이미지를 다운 받는다고 가정합니다.

이때 이미지의 불변성 때문에 수백MB ~ 수백GB가 되는 이미지를 다시 다운로드 받는 것은 매우 비효율적입니다. 도커는 이러한 문제를 해결하기 위해 레이어(Layer)라는 개념을 도입했습니다.

레이어란 수정사항이나 추가적인 파일이 있을 때 다시 다운로드 받는 것이 아닌 파일을 추가하는 개념입니다. 아래의 사진과 같이 기존의 이미지가 존재하는데 새로운 이미지를 다운 받을 경우 레이어만 따로 빼 파일을 추가하는 형식입니다.

도커 이미지는 위 그림처럼 여러 개의 읽기 전요(read-only) 레이어로 구성되고, 파일이 추가되면 새로운 레이어가 생성됩니다. 이러한 개념은 git 레포지토리에 commit 로그를 쌓는 것과 같다고 볼 수 있습니다.

3. 프로젝트 생성

이제 이론을 바탕으로 실전에 적용해볼 차례입니다. 간단한 FastAPI 프로젝트를 생성하고 docker를 이용하여 컨테이너 이미지를 빌드해보겠습니다.

새로운 폴더를 만들고 가상환경 생성 후 활성화합니다.

python -m venv venv

source venv/scripts/activate

이후 필요한 라이브러리를 설치하고 종속성을 파일에 저장합니다.

pip install "fastapi[all]"

pip freeze > requirements.txt

app 폴더를 생성 후 해당 폴더에 main.py 파일을 만들고 루트 도메인에 간단한 문구를 출력하는 FastAPI 서버 코드를 작성해보겠습니다.

from fastapi import FastAPI

app = FastAPI()

@app.get('/')
def welcome_root():
    return "Welcome to root"

다음과 같은 명령어로 실행하면 서버가 잘 동작하는 것을 확인할 수 있습니다.

uvicorn app.main:app --reload

4. Dockerfile 생성

루트 디렉토리에 Dockerfile을 생성하고 다음과 같이 입력합니다.

FROM python:3.11

WORKDIR /test

COPY ./requirements.txt /test/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /test/requirements.txt

COPY ./app /test/app

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

여기서 중요한 것은 다음과 같은 코드입니다.

COPY ./requirements.txt /test/requirements.txt

먼저 코드의 나머지 부분이 아닌 종속성을 모아둔 파일을 dockerfile에 복사합니다. 이것은 위에선 언급했던 레이어 개념이 적용된 부분입니다.

도커는 컨테이너 이미지를 점진적으로 구축하여 수정사항이나 새로운 파일이 추가되면 기존의 레이어에 추가되어 구축됩니다. 이때 도커가 수정되지 않은 레이어를 발견하면 캐시에서 해당 레이어를 재사용하여 시간을 절약할 수 있습니다.

즉, 종속성을 저장하는 파일인 requirements.txt는 소스코드에 비해 자주 수정되는 파일이 아니기 때문에 소스 파일 위에 종속성 파일을 설치하는 명령어를 배치하는 것이 더 효율적입니다.

모든 작업이 끝났다면 프로젝트 구조는 다음과 같습니다.

.
├── app
│   ├── __init__.py
│   └── main.py
├── Dockerfile
└── requirements.txt

5. Docker 이미지 빌드

이제 도커 이미지를 빌드하고 실행합니다.

docker build -t test-image:latest .

빌드된 시간을 보면 23.9초가 걸린 것을 확인할 수 있습니다.

이후 소스코드를 변경한 후 다시 이미지를 빌드해보겠습니다.

from fastapi import FastAPI

app = FastAPI()

@app.get('/')
def welcome_root():
    return "Hello world!"

중간에 CACHED 라는 명령어 줄을 확인해보면 캐시가 된 것을 확인할 수 있고 빌드 시간이 16.5초로 확실히 감소된 것을 확인할 수 있습니다.

만약 방금 생성한 이미지에서 컨테이너를 실행하려면 다음과 같은 명령어로 실행합니다.

docker run -p 8000:8000 test-image

reference

profile
높이보다는 멀리, 넓게보다는 깊게

0개의 댓글