콘텐츠로 이동

컨테이너의 FastAPI - 도커

FastAPI 어플리케이션을 배포할 때 일반적인 접근 방법은 리눅스 컨테이너 이미지를 생성하는 것입니다. 이 방법은 주로 도커를 사용해 이루어집니다. 그런 다음 해당 컨테이너 이미지를 몇가지 방법으로 배포할 수 있습니다.

리눅스 컨테이너를 사용하는 데에는 보안, 반복 가능성, 단순함 등의 장점이 있습니다.

시간에 쫓기고 있고 이미 이런것들을 알고 있다면 Dockerfile👇로 점프할 수 있습니다.

도커파일 미리보기 👀
FROM python:3.9

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

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

COPY ./app /code/app

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

# If running behind a proxy like Nginx or Traefik add --proxy-headers
# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"]

컨테이너란

컨테이너(주로 리눅스 컨테이너)는 어플리케이션의 의존성과 필요한 파일들을 모두 패키징하는 매우 가벼운 방법입니다. 컨테이너는 같은 시스템에 있는 다른 컨테이너(다른 어플리케이션이나 요소들)와 독립적으로 유지됩니다.

리눅스 컨테이너는 호스트(머신, 가상 머신, 클라우드 서버 등)와 같은 리눅스 커널을 사용해 실행됩니다. 이말은 리눅스 컨테이너가 (전체 운영체제를 모방하는 다른 가상 머신과 비교했을 때) 매우 가볍다는 것을 의미합니다.

이 방법을 통해, 컨테이너는 직접 프로세스를 실행하는 것과 비슷한 정도의 적은 자원을 소비합니다 (가상 머신은 훨씬 많은 자원을 소비할 것입니다).

컨테이너는 또한 그들만의 독립된 실행 프로세스 (일반적으로 하나의 프로세스로 충분합니다), 파일 시스템, 그리고 네트워크를 가지므로 배포, 보안, 개발 및 기타 과정을 단순화 합니다.

컨테이너 이미지란

컨테이너컨테이너 이미지를 실행한 것 입니다.

컨테이너 이미지란 컨테이너에 필요한 모든 파일, 환경 변수 그리고 디폴트 명령/프로그램의 정적 버전입니다. 여기서 정적이란 말은 컨테이너 이미지가 작동되거나 실행되지 않으며, 단지 패키지 파일과 메타 데이터라는 것을 의미합니다.

저장된 정적 컨텐츠인 컨테이너 이미지와 대조되게, 컨테이너란 보통 실행될 수 있는 작동 인스턴스를 의미합니다.

컨테이너가 (컨테이너 이미지로 부터) 시작되고 실행되면, 컨테이너는 파일이나 환경 변수를 생성하거나 변경할 수 있습니다. 이러한 변화는 오직 컨테이너에서만 존재하며, 그 기반이 되는 컨테이너 이미지에는 지속되지 않습니다 (즉 디스크에는 저장되지 않습니다).

컨테이너 이미지는 프로그램 파일과 컨텐츠, 즉 python과 어떤 파일 main.py에 비교할 수 있습니다.

그리고 (컨테이너 이미지와 대비해서) 컨테이너는 이미지의 실제 실행 인스턴스로 프로세스에 비교할 수 있습니다. 사실, 컨테이너는 프로세스 러닝이 있을 때만 실행됩니다 (그리고 보통 하나의 프로세스 입니다). 컨테이너는 내부에서 실행되는 프로세스가 없으면 종료됩니다.

컨테이너 이미지

도커는 컨테이너 이미지컨테이너를 생성하고 관리하는데 주요 도구 중 하나가 되어왔습니다.

그리고 도커 허브에 다양한 도구, 환경, 데이터베이스, 그리고 어플리케이션에 대해 미리 만들어진 공식 컨테이너 이미지가 공개되어 있습니다.

예를 들어, 공식 파이썬 이미지가 있습니다.

또한 다른 대상, 예를 들면 데이터베이스를 위한 이미지들도 있습니다:

미리 만들어진 컨테이너 이미지를 사용하면 서로 다른 도구들을 결합하기 쉽습니다. 대부분의 경우에, 공식 이미지들을 사용하고 환경 변수를 통해 설정할 수 있습니다.

이런 방법으로 대부분의 경우에 컨테이너와 도커에 대해 배울 수 있으며 다양한 도구와 요소들에 대한 지식을 재사용할 수 있습니다.

따라서, 서로 다른 다중 컨테이너를 생성한 다음 이들을 연결할 수 있습니다. 예를 들어 데이터베이스, 파이썬 어플리케이션, 리액트 프론트엔드 어플리케이션을 사용하는 웹 서버에 대한 컨테이너를 만들어 이들의 내부 네트워크로 각 컨테이너를 연결할 수 있습니다.

모든 컨테이너 관리 시스템(도커나 쿠버네티스)은 이러한 네트워킹 특성을 포함하고 있습니다.

컨테이너와 프로세스

컨테이너 이미지는 보통 컨테이너를 시작하기 위해 필요한 메타데이터와 디폴트 커맨드/프로그램과 그 프로그램에 전달하기 위한 파라미터들을 포함합니다. 이는 커맨드 라인에서 프로그램을 실행할 때 필요한 값들과 유사합니다.

컨테이너가 시작되면, 해당 커맨드/프로그램이 실행됩니다 (그러나 다른 커맨드/프로그램을 실행하도록 오버라이드 할 수 있습니다).

컨테이너는 메인 프로세스(커맨드 또는 프로그램)이 실행되는 동안 실행됩니다.

컨테이너는 일반적으로 단일 프로세스를 가지고 있지만, 메인 프로세스의 서브 프로세스를 시작하는 것도 가능하며, 이 방법으로 하나의 컨테이너에 다중 프로세스를 가질 수 있습니다.

그러나 최소한 하나의 실행중인 프로세스를 가지지 않고서는 실행중인 컨테이너를 가질 수 없습니다. 만약 메인 프로세스가 중단되면, 컨테이너도 중단됩니다.

FastAPI를 위한 도커 이미지 빌드하기

이제 무언가를 만들어 봅시다! 🚀

공식 파이썬 이미지에 기반하여, FastAPI를 위한 도커 이미지맨 처음부터 생성하는 방법을 보이겠습니다.

대부분의 경우에 다음과 같은 것들을 하게 됩니다. 예를 들면:

  • 쿠버네티스 또는 유사한 도구 사용하기
  • 라즈베리 파이로 실행하기
  • 컨테이너 이미지를 실행할 클라우드 서비스 사용하기 등

요구 패키지

일반적으로는 어플리케이션의 특정 파일을 위한 패키지 요구 조건이 있을 것입니다.

그 요구 조건을 설치하는 방법은 여러분이 사용하는 도구에 따라 다를 것입니다.

가장 일반적인 방법은 패키지 이름과 버전이 줄 별로 기록된 requirements.txt 파일을 만드는 것입니다.

버전의 범위를 설정하기 위해서는 FastAPI 버전들에 대하여에 쓰여진 것과 같은 아이디어를 사용합니다.

예를 들어, requirements.txt 파일은 다음과 같을 수 있습니다:

fastapi>=0.68.0,<0.69.0
pydantic>=1.8.0,<2.0.0
uvicorn>=0.15.0,<0.16.0

그리고 일반적으로 패키지 종속성은 pip로 설치합니다. 예를 들어:

$ pip install -r requirements.txt
---> 100%
Successfully installed fastapi pydantic uvicorn

정보

패키지 종속성을 정의하고 설치하기 위한 방법과 도구는 다양합니다.

나중에 아래 세션에서 Poetry를 사용한 예시를 보이겠습니다. 👇

FastAPI 코드 생성하기

  • app 디렉터리를 생성하고 이동합니다.
  • 빈 파일 __init__.py을 생성합니다.
  • 다음과 같은 main.py을 생성합니다:
from typing import Union

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

도커파일

이제 같은 프로젝트 디렉터리에 다음과 같은 파일 Dockerfile을 생성합니다:

# (1)
FROM python:3.9

# (2)
WORKDIR /code

# (3)
COPY ./requirements.txt /code/requirements.txt

# (4)
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# (5)
COPY ./app /code/app

# (6)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
  1. 공식 파이썬 베이스 이미지에서 시작합니다.

  2. 현재 워킹 디렉터리를 /code로 설정합니다.

    여기에 requirements.txt 파일과 app 디렉터리를 위치시킬 것입니다.

  3. 요구 조건과 파일을 /code 디렉터리로 복사합니다.

    처음에는 오직 요구 조건이 필요한 파일만 복사하고, 이외의 코드는 그대로 둡니다.

    이 파일이 자주 바뀌지 않기 때문에, 도커는 파일을 탐지하여 이 단계의 캐시를 사용하여 다음 단계에서도 캐시를 사용할 수 있도록 합니다.

  4. 요구 조건 파일에 있는 패키지 종속성을 설치합니다.

    --no-cache-dir 옵션은 pip에게 다운로드한 패키지들을 로컬 환경에 저장하지 않도록 전달합니다. 이는 마치 같은 패키지를 설치하기 위해 오직 pip만 다시 실행하면 될 것 같지만, 컨테이너로 작업하는 경우 그렇지는 않습니다.

    노트

    --no-cache-dir 는 오직 pip와 관련되어 있으며, 도커나 컨테이너와는 무관합니다.

    --upgrade 옵션은 pip에게 설치된 패키지들을 업데이트하도록 합니다.

    이전 단계에서 파일을 복사한 것이 도커 캐시에 의해 탐지되기 때문에, 이 단계에서도 가능한 한 도커 캐시를 사용하게 됩니다.

    이 단계에서 캐시를 사용하면 매번 모든 종속성을 다운로드 받고 설치할 필요가 없어, 개발 과정에서 이미지를 지속적으로 생성하는 데에 드는 시간을 많이 절약할 수 있습니다.

  5. /code 디렉터리에 ./app 디렉터리를 복사합니다.

    자주 변경되는 모든 코드를 포함하고 있기 때문에, 도커 캐시는 이 단계나 이후의 단계에서 잘 사용되지 않습니다.

    그러므로 컨테이너 이미지 빌드 시간을 최적화하기 위해 Dockerfile거의 끝 부분에 입력하는 것이 중요합니다.

  6. uvicorn 서버를 실행하기 위해 커맨드를 설정합니다.

    CMD는 문자열 리스트를 입력받고, 각 문자열은 커맨드 라인의 각 줄에 입력할 문자열입니다.

    이 커맨드는 현재 워킹 디렉터리에서 실행되며, 이는 위에서 WORKDIR /code로 설정한 /code 디렉터리와 같습니다.

    프로그램이 /code에서 시작하고 그 속에 ./app 디렉터리가 여러분의 코드와 함께 들어있기 때문에, Uvicorn은 이를 보고 appapp.main으로부터 불러 올 것입니다.

각 코드 라인을 코드의 숫자 버블을 클릭하여 리뷰할 수 있습니다. 👆

이제 여러분은 다음과 같은 디렉터리 구조를 가지고 있을 것입니다:

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

TLS 종료 프록시의 배후

만약 여러분이 컨테이너를 Nginx 또는 Traefik과 같은 TLS 종료 프록시 (로드 밸런서) 뒤에서 실행하고 있다면, --proxy-headers 옵션을 더하는 것이 좋습니다. 이 옵션은 Uvicorn에게 어플리케이션이 HTTPS 등의 뒤에서 실행되고 있으므로 프록시에서 전송된 헤더를 신뢰할 수 있다고 알립니다.

CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]

도커 캐시

Dockerfile에는 중요한 트릭이 있는데, 처음에는 의존성이 있는 파일만 복사하고, 나머지 코드는 그대로 둡니다. 왜 이런 방법을 써야하는지 설명하겠습니다.

COPY ./requirements.txt /code/requirements.txt

도커와 다른 도구들은 컨테이너 이미지를 증가하는 방식으로 빌드합니다. Dockerfile의 맨 윗 부분부터 시작해, 레이어 위에 새로운 레이어를 더하는 방식으로, Dockerfile의 각 지시 사항으로 부터 생성된 어떤 파일이든 더해갑니다.

도커 그리고 이와 유사한 도구들은 이미지 생성 시에 내부 캐시를 사용합니다. 만약 어떤 파일이 마지막으로 컨테이너 이미지를 빌드한 때로부터 바뀌지 않았다면, 파일을 다시 복사하여 새로운 레이어를 처음부터 생성하는 것이 아니라, 마지막에 생성했던 같은 레이어를 재사용합니다.

단지 파일 복사를 지양하는 것으로 효율이 많이 향상되는 것은 아니지만, 그 단계에서 캐시를 사용했기 때문에, 다음 단계에서도 마찬가지로 캐시를 사용할 수 있습니다. 예를 들어, 다음과 같은 의존성을 설치하는 지시 사항을 위한 캐시를 사용할 수 있습니다:

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

패키지를 포함하는 파일은 자주 변경되지 않습니다. 따라서 해당 파일만 복사하므로서, 도커는 그 단계의 캐시를 사용할 수 있습니다.

그 다음으로, 도커는 다음 단계에서 의존성을 다운로드하고 설치하는 캐시를 사용할 수 있게 됩니다. 바로 이 과정에서 우리는 많은 시간을 절약하게 됩니다. ✨ ...그리고 기다리는 지루함도 피할 수 있습니다. 😪😆

패키지 의존성을 다운로드 받고 설치하는 데이는 수 분이 걸릴 수 있지만, 캐시를 사용하면 최대 수 초만에 끝낼 수 있습니다.

또한 여러분이 개발 과정에서 코드의 변경 사항이 반영되었는지 확인하기 위해 컨테이너 이미지를 계속해서 빌드하면, 절약된 시간은 축적되어 더욱 커질 것입니다.

그리고 나서 Dockerfile의 거의 끝 부분에서, 모든 코드를 복사합니다. 이것이 가장 빈번하게 변경되는 부분이며, 대부분의 경우에 이 다음 단계에서는 캐시를 사용할 수 없기 때문에 가장 마지막에 둡니다.

COPY ./app /code/app

도커 이미지 생성하기

이제 모든 파일이 제자리에 있으니, 컨테이너 이미지를 빌드합니다.

  • (여러분의 Dockerfileapp 디렉터리가 위치한) 프로젝트 디렉터리로 이동합니다.
  • FastAPI 이미지를 빌드합니다:
$ docker build -t myimage .

---> 100%

맨 끝에 있는 . 에 주목합시다. 이는 ./와 동등하며, 도커에게 컨테이너 이미지를 빌드하기 위한 디렉터리를 알려줍니다.

이 경우에는 현재 디렉터리(.)와 같습니다.

도커 컨테이너 시작하기

  • 여러분의 이미지에 기반하여 컨테이너를 실행합니다:
$ docker run -d --name mycontainer -p 80:80 myimage

체크하기

여러분의 도커 컨테이너 URL에서 실행 사항을 체크할 수 있습니다. 예를 들어: http://192.168.99.100/items/5?q=somequery 또는 http://127.0.0.1/items/5?q=somequery (또는 동일하게, 여러분의 도커 호스트를 이용해서 체크할 수도 있습니다).

아래와 비슷한 것을 보게 될 것입니다:

{"item_id": 5, "q": "somequery"}

인터랙티브 API 문서

이제 여러분은 http://192.168.99.100/docs 또는 http://127.0.0.1/docs로 이동할 수 있습니다(또는, 여러분의 도커 호스트를 이용할 수 있습니다).

여러분은 자동으로 생성된 인터랙티브 API(Swagger UI에서 제공된)를 볼 수 있습니다:

Swagger UI

대안 API 문서

또한 여러분은 http://192.168.99.100/redoc 또는 http://127.0.0.1/redoc으로 이동할 수 있습니다(또는, 여러분의 도커 호스트를 이용할 수 있습니다).

여러분은 자동으로 생성된 대안 문서(ReDoc에서 제공된)를 볼 수 있습니다:

ReDoc

단일 파일 FastAPI로 도커 이미지 생성하기

만약 여러분의 FastAPI가 하나의 파일이라면, 예를 들어 ./app 디렉터리 없이 main.py 파일만으로 이루어져 있다면, 파일 구조는 다음과 유사할 것입니다:

.
├── Dockerfile
├── main.py
└── requirements.txt

그러면 여러분들은 Dockerfile 내에 있는 파일을 복사하기 위해 그저 상응하는 경로를 바꾸기만 하면 됩니다:

FROM python:3.9

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

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

# (1)
COPY ./main.py /code/

# (2)
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
  1. main.py 파일을 /code 디렉터리로 곧바로 복사합니다(./app 디렉터리는 고려하지 않습니다).

  2. Uvicorn을 실행해 app 객체를 (app.main 대신) main으로 부터 불러오도록 합니다.

그 다음 Uvicorn 커맨드를 조정해서 FastAPI 객체를 불러오는데 app.main 대신에 새로운 모듈 main을 사용하도록 합니다.

배포 개념

이제 컨테이너의 측면에서 배포 개념에서 다루었던 것과 같은 배포 개념에 대해 이야기해 보겠습니다.

컨테이너는 주로 어플리케이션을 빌드하고 배포하기 위한 과정을 단순화하는 도구이지만, 배포 개념에 대한 특정한 접근법을 강요하지 않기 때문에 가능한 배포 전략에는 여러가지가 있습니다.

좋은 소식은 서로 다른 전략들을 포괄하는 배포 개념이 있다는 점입니다. 🎉

컨테이너 측면에서 배포 개념을 리뷰해 보겠습니다:

  • HTTPS
  • 구동하기
  • 재시작
  • 복제 (실행 중인 프로세스 개수)
  • 메모리
  • 시작하기 전 단계들

HTTPS

만약 우리가 FastAPI 어플리케이션을 위한 컨테이너 이미지에만 집중한다면 (그리고 나중에 실행될 컨테이너에), HTTPS는 일반적으로 다른 도구에 의해 외부적으로 다루어질 것 입니다.

HTTPS인증서자동 취득을 다루는 것은 다른 컨테이너가 될 수 있는데, 예를 들어 Traefik을 사용하는 것입니다.

Traefik은 도커, 쿠버네티스, 그리고 다른 도구와 통합되어 있어 여러분의 컨테이너를 포함하는 HTTPS를 셋업하고 설정하는 것이 매우 쉽습니다.

대안적으로, HTTPS는 클라우드 제공자에 의해 서비스의 일환으로 다루어질 수도 있습니다 (이때도 어플리케이션은 여전히 컨테이너에서 실행될 것입니다).

구동과 재시작

여러분의 컨테이너를 시작하고 실행하는 데에 일반적으로 사용되는 도구는 따로 있습니다.

이는 도커 자체일 수도 있고, 도커 컴포즈, 쿠버네티스, 클라우드 서비스 등이 될 수 있습니다.

대부분 (또는 전체) 경우에, 컨테이너를 구동하거나 고장시에 재시작하도록 하는 간단한 옵션이 있습니다. 예를 들어, 도커에서는, 커맨드 라인 옵션 --restart 입니다.

컨테이너를 사용하지 않고서는, 어플리케이션을 구동하고 재시작하는 것이 매우 번거롭고 어려울 수 있습니다. 하지만 컨테이너를 사용한다면 대부분의 경우에 이런 기능은 기본적으로 포함되어 있습니다. ✨

복제 - 프로세스 개수

만약 여러분이 쿠버네티스와 머신 클러스터, 도커 스왐 모드, 노마드, 또는 다른 여러 머신 위에 분산 컨테이너를 관리하는 복잡한 시스템을 다루고 있다면, 여러분은 각 컨테이너에서 (워커와 함께 사용하는 Gunicorn 같은) 프로세스 매니저 대신 클러스터 레벨에서 복제를 다루고 싶을 것입니다.

쿠버네티스와 같은 분산 컨테이너 관리 시스템 중 일부는 일반적으로 들어오는 요청에 대한 로드 밸런싱을 지원하면서 컨테이너 복제를 다루는 통합된 방법을 가지고 있습니다. 모두 클러스터 레벨에서 말이죠.

이런 경우에, 여러분은 위에서 묘사된 것처럼 처음부터 도커 이미지를 빌드해서, 의존성을 설치하고, Uvicorn 워커를 관리하는 Gunicorn 대신 단일 Uvicorn 프로세스를 실행하고 싶을 것입니다.

로드 밸런서

컨테이너로 작업할 때, 여러분은 일반적으로 메인 포트의 상황을 감지하는 요소를 가지고 있을 것입니다. 이는 HTTPS를 다루는 TLS 종료 프록시와 같은 다른 컨테이너일 수도 있고, 유사한 다른 도구일 수도 있습니다.

이 요소가 요청들의 로드를 읽어들이고 각 워커에게 (바라건대) 균형적으로 분배한다면, 이 요소는 일반적으로 로드 밸런서라고 불립니다.

HTTPS를 위해 사용된 TLS 종료 프록시 요소 또한 로드 밸런서가 될 수 있습니다.

또한 컨테이너로 작업할 때, 컨테이너를 시작하고 관리하기 위해 사용한 것과 동일한 시스템은 이미 해당 로드 밸런서로 부터 여러분의 앱에 해당하는 컨테이너로 네트워크 통신(예를 들어, HTTP 요청)을 전송하는 내부적인 도구를 가지고 있을 것입니다 (여기서도 로드 밸런서는 TLS 종료 프록시일 수 있습니다).

하나의 로드 밸런서 - 다중 워커 컨테이너

쿠버네티스나 또는 다른 분산 컨테이너 관리 시스템으로 작업할 때, 시스템 내부의 네트워킹 메커니즘을 이용함으로써 메인 포트를 감지하고 있는 단일 로드 밸런서는 여러분의 앱에서 실행되고 있는 여러개의 컨테이너에 통신(요청들)을 전송할 수 있게 됩니다.

여러분의 앱에서 실행되고 있는 각각의 컨테이너는 일반적으로 하나의 프로세스만 가질 것입니다 (예를 들어, FastAPI 어플리케이션에서 실행되는 하나의 Uvicorn 프로세스처럼). 이 컨테이너들은 모두 같은 것을 실행하는 점에서 동일한 컨테이너이지만, 프로세스, 메모리 등은 공유하지 않습니다. 이 방식으로 여러분은 CPU의 서로 다른 코어들 또는 서로 다른 머신들병렬화하는 이점을 얻을 수 있습니다.

또한 로드 밸런서가 있는 분산 컨테이너 시스템은 여러분의 앱에 있는 컨테이너 각각에 차례대로 요청을 분산시킬 것 입니다. 따라서 각 요청은 여러분의 앱에서 실행되는 여러개의 복제된 컨테이너들 중 하나에 의해 다루어질 것 입니다.

그리고 일반적으로 로드 밸런서는 여러분의 클러스터에 있는 다른 앱으로 가는 요청들도 다룰 수 있으며 (예를 들어, 다른 도메인으로 가거나 다른 URL 경로 접두사를 가지는 경우), 이 통신들을 클러스터에 있는 바로 그 다른 어플리케이션으로 제대로 전송할 수 있습니다.

단일 프로세스를 가지는 컨테이너

이 시나리오의 경우, 여러분은 이미 클러스터 레벨에서 복제를 다루고 있을 것이므로 컨테이너 당 단일 (Uvicorn) 프로세스를 가지고자 할 것입니다.

따라서, 여러분은 Gunicorn 이나 Uvicorn 워커, 또는 Uvicorn 워커를 사용하는 Uvicorn 매니저와 같은 프로세스 매니저를 가지고 싶어하지 않을 것입니다. 여러분은 컨테이너 당 단일 Uvicorn 프로세스를 가지고 싶어할 것입니다 (그러나 아마도 다중 컨테이너를 가질 것입니다).

이미 여러분이 클러스터 시스템을 관리하고 있으므로, (Uvicorn 워커를 관리하는 Gunicorn 이나 Uvicorn 처럼) 컨테이너 내에 다른 프로세스 매니저를 가지는 것은 불필요한 복잡성만 더하게 될 것입니다.

다중 프로세스를 가지는 컨테이너와 특수한 경우들

당연한 말이지만, 여러분이 내부적으로 Uvicorn 워커 프로세스들를 시작하는 Gunicorn 프로세스 매니저를 가지는 단일 컨테이너를 원하는 특수한 경우도 있을 것입니다.

그런 경우에, 여러분들은 Gunicorn을 프로세스 매니저로 포함하는 공식 도커 이미지를 사용할 수 있습니다. 이 프로세스 매니저는 다중 Uvicorn 워커 프로세스들을 실행하며, 디폴트 세팅으로 현재 CPU 코어에 기반하여 자동으로 워커 개수를 조정합니다. 이 사항에 대해서는 아래의 Gunicorn과 함께하는 공식 도커 이미지 - Uvicorn에서 더 다루겠습니다.

이런 경우에 해당하는 몇가지 예시가 있습니다:

단순한 앱

만약 여러분의 어플리케이션이 충분히 단순해서 (적어도 아직은) 프로세스 개수를 파인-튠 할 필요가 없거나 클러스터가 아닌 단일 서버에서 실행하고 있다면, 여러분은 컨테이너 내에 프로세스 매니저를 사용하거나 (공식 도커 이미지에서) 자동으로 설정되는 디폴트 값을 사용할 수 있습니다.

도커 구성

여러분은 도커 컴포즈로 (클러스터가 아닌) 단일 서버로 배포할 수 있으며, 이 경우에 공유된 네트워크와 로드 밸런싱을 포함하는 (도커 컴포즈로) 컨테이너의 복제를 관리하는 단순한 방법이 없을 수도 있습니다.

그렇다면 여러분은 프로세스 매니저와 함께 내부에 몇개의 워커 프로세스들을 시작하는 단일 컨테이너를 필요로 할 수 있습니다.

Prometheus와 다른 이유들

여러분은 단일 프로세스를 가지는 다중 컨테이너 대신 다중 프로세스를 가지는 단일 컨테이너를 채택하는 다른 이유가 있을 수 있습니다.

예를 들어 (여러분의 장치 설정에 따라) Prometheus 익스포터와 같이 같은 컨테이너에 들어오는 각 요청에 대해 접근권한을 가지는 도구를 사용할 수 있습니다.

이 경우에 여러분이 여러개의 컨테이너들을 가지고 있다면, Prometheus가 메트릭을 읽어 들일 때, 디폴트로 매번 하나의 컨테이너(특정 리퀘스트를 관리하는 바로 그 컨테이너)로 부터 읽어들일 것입니다. 이는 모든 복제된 컨테이너에 대해 축적된 메트릭들을 읽어들이는 것과 대비됩니다.

그렇다면 이 경우에는 다중 프로세스를 가지는 하나의 컨테이너를 두어서 같은 컨테이너에서 모든 내부 프로세스에 대한 Prometheus 메트릭을 수집하는 로컬 도구(예를 들어 Prometheus 익스포터 같은)를 두어서 이 메그릭들을 하나의 컨테이너에 내에서 공유하는 방법이 더 단순할 것입니다.


요점은, 이 중의 어느것도 여러분들이 반드시 따라야하는 확정된 사실이 아니라는 것입니다. 여러분은 이 아이디어들을 여러분의 고유한 이용 사례를 평가하는데 사용하고, 여러분의 시스템에 가장 적합한 접근법이 어떤 것인지 결정하며, 다음의 개념들을 관리하는 방법을 확인할 수 있습니다:

  • 보안 - HTTPS
  • 구동하기
  • 재시작
  • 복제 (실행 중인 프로세스 개수)
  • 메모리
  • 시작하기 전 단계들

메모리

만약 여러분이 컨테이너 당 단일 프로세스를 실행한다면, 여러분은 각 컨테이너(복제된 경우에는 여러개의 컨테이너들)에 대해 잘 정의되고, 안정적이며, 제한된 용량의 메모리 소비량을 가지고 있을 것입니다.

그러면 여러분의 컨테이너 관리 시스템(예를 들어 쿠버네티스) 설정에서 앞서 정의된 것과 같은 메모리 제한과 요구사항을 설정할 수 있습니다. 이런 방법으로 가용 머신이 필요로하는 메모리와 클러스터에 있는 가용 머신들을 염두에 두고 컨테이너를 복제할 수 있습니다.

만약 여러분의 어플리케이션이 단순하다면, 이것은 문제가 되지 않을 것이고, 고정된 메모리 제한을 구체화할 필요도 없을 것입니다. 하지만 여러분의 어플리케이션이 (예를 들어 머신 러닝 모델같이) 많은 메모리를 소요한다면, 어플리케이션이 얼마나 많은 양의 메모리를 사용하는지 확인하고 각 머신에서 사용하는 컨테이너의 수를 조정할 필요가 있습니다 (그리고 필요에 따라 여러분의 클러스터에 머신을 추가할 수 있습니다).

만약 여러분이 컨테이너 당 여러개의 프로세스를 실행한다면 (예를 들어 공식 도커 이미지 처럼), 여러분은 시작된 프로세스 개수가 가용한 것 보다 더 많은 메모리를 소비하지 않는지 확인해야 합니다.

시작하기 전 단계들과 컨테이너

만약 여러분이 컨테이너(예를 들어 도커, 쿠버네티스)를 사용한다면, 여러분이 접근할 수 있는 주요 방법은 크게 두가지가 있습니다.

다중 컨테이너

만약 여러분이 여러개의 컨테이너를 가지고 있다면, 아마도 각각의 컨테이너는 하나의 프로세스를 가지고 있을 것입니다(예를 들어, 쿠버네티스 클러스터에서). 그러면 여러분은 복제된 워커 컨테이너를 실행하기 이전에, 하나의 컨테이너에 있는 이전의 단계들을 수행하는 단일 프로세스를 가지는 별도의 컨테이너들을 가지고 싶을 것입니다.

정보

만약 여러분이 쿠버네티스를 사용하고 있다면, 아마도 이는 Init Container일 것입니다.

만약 여러분의 이용 사례에서 이전 단계들을 병렬적으로 여러번 수행하는데에 문제가 없다면 (예를 들어 데이터베이스 이전을 실행하지 않고 데이터베이스가 준비되었는지 확인만 하는 경우), 메인 프로세스를 시작하기 전에 이 단계들을 각 컨테이너에 넣을 수 있습니다.

단일 컨테이너

만약 여러분의 셋업이 다중 프로세스(또는 하나의 프로세스)를 시작하는 하나의 컨테이너를 가지는 단순한 셋업이라면, 사전 단계들을 앱을 포함하는 프로세스를 시작하기 직전에 같은 컨테이너에서 실행할 수 있습니다. 공식 도커 이미지는 이를 내부적으로 지원합니다.

Gunicorn과 함께하는 공식 도커 이미지 - Uvicorn

앞 챕터에서 자세하게 설명된 것 처럼, Uvicorn 워커와 같이 실행되는 Gunicorn을 포함하는 공식 도커 이미지가 있습니다: 서버 워커 - Uvicorn과 함께하는 Gunicorn.

이 이미지는 주로 위에서 설명된 상황에서 유용할 것입니다: 다중 프로세스를 가지는 컨테이너와 특수한 경우들.

경고

여러분이 이 베이스 이미지 또는 다른 유사한 이미지를 필요로 하지 않을 높은 가능성이 있으며, 위에서 설명된 것처럼: FastAPI를 위한 도커 이미지 빌드하기 처음부터 이미지를 빌드하는 것이 더 나을 수 있습니다.

이 이미지는 가능한 CPU 코어에 기반한 몇개의 워커 프로세스를 설정하는 자동-튜닝 메커니즘을 포함하고 있습니다.

이 이미지는 민감한 디폴트 값을 가지고 있지만, 여러분들은 여전히 환경 변수 또는 설정 파일을 통해 설정값을 수정하고 업데이트 할 수 있습니다.

또한 스크립트를 통해 시작하기 전 사전 단계를 실행하는 것을 지원합니다.

모든 설정과 옵션을 보려면, 도커 이미지 페이지로 이동합니다: tiangolo/uvicorn-gunicorn-fastapi.

공식 도커 이미지에 있는 프로세스 개수

이 이미지에 있는 프로세스 개수는 가용한 CPU 코어들로 부터 자동으로 계산됩니다.

이것이 의미하는 바는 이미지가 CPU로부터 최대한의 성능쥐어짜낸다는 것입니다.

여러분은 이 설정 값을 환경 변수나 기타 방법들로 조정할 수 있습니다.

그러나 프로세스의 개수가 컨테이너가 실행되고 있는 CPU에 의존한다는 것은 또한 소요되는 메모리의 크기 또한 이에 의존한다는 것을 의미합니다.

그렇기 때문에, 만약 여러분의 어플리케이션이 많은 메모리를 요구하고 (예를 들어 머신러닝 모델처럼), 여러분의 서버가 CPU 코어 수는 많지만 적은 메모리를 가지고 있다면, 여러분의 컨테이너는 가용한 메모리보다 많은 메모리를 사용하려고 시도할 수 있으며, 결국 퍼포먼스를 크게 떨어뜨릴 수 있습니다(심지어 고장이 날 수도 있습니다). 🚨

Dockerfile 생성하기

이 이미지에 기반해 Dockerfile을 생성하는 방법은 다음과 같습니다:

FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9

COPY ./requirements.txt /app/requirements.txt

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

COPY ./app /app

더 큰 어플리케이션

만약 여러분이 다중 파일을 가지는 더 큰 어플리케이션을 생성하는 섹션을 따랐다면, 여러분의 Dockerfile은 대신 이렇게 생겼을 것입니다:

FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9

COPY ./requirements.txt /app/requirements.txt

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

COPY ./app /app/app

언제 사용할까

여러분들이 쿠버네티스(또는 유사한 다른 도구) 사용하거나 클러스터 레벨에서 다중 컨테이너를 이용해 이미 사본을 설정하고 있다면, 공식 베이스 이미지(또는 유사한 다른 이미지)를 사용하지 않는 것 좋습니다. 그런 경우에 여러분은 다음에 설명된 것 처럼 처음부터 이미지를 빌드하는 것이 더 낫습니다: FastAPI를 위한 도커 이미지 빌드하기.

이 이미지는 위의 다중 프로세스를 가지는 컨테이너와 특수한 경우들에서 설명된 특수한 경우에 대해서만 주로 유용할 것입니다. 예를 들어, 만약 여러분의 어플리케이션이 충분히 단순해서 CPU에 기반한 디폴트 프로세스 개수를 설정하는 것이 잘 작동한다면, 클러스터 레벨에서 수동으로 사본을 설정할 필요가 없을 것이고, 여러분의 앱에서 하나 이상의 컨테이너를 실행하지도 않을 것입니다. 또는 만약에 여러분이 도커 컴포즈로 배포하거나, 단일 서버에서 실행하거나 하는 경우에도 마찬가지입니다.

컨테이너 이미지 배포하기

컨테이너 (도커) 이미지를 완성한 뒤에 이를 배포하는 방법에는 여러가지 방법이 있습니다.

예를 들어:

  • 단일 서버에서 도커 컴포즈로 배포하기
  • 쿠버네티스 클러스터로 배포하기
  • 도커 스왐 모드 클러스터로 배포하기
  • 노마드 같은 다른 도구로 배포하기
  • 여러분의 컨테이너 이미지를 배포해주는 클라우드 서비스로 배포하기

Poetry의 도커 이미지

만약 여러분들이 프로젝트 의존성을 관리하기 위해 Poetry를 사용한다면, 도커의 멀티-스테이지 빌딩을 사용할 수 있습니다:

# (1)
FROM python:3.9 as requirements-stage

# (2)
WORKDIR /tmp

# (3)
RUN pip install poetry

# (4)
COPY ./pyproject.toml ./poetry.lock* /tmp/

# (5)
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes

# (6)
FROM python:3.9

# (7)
WORKDIR /code

# (8)
COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt

# (9)
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# (10)
COPY ./app /code/app

# (11)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
  1. 첫 스테이지로, requirements-stage라고 이름 붙였습니다.

  2. /tmp를 현재의 워킹 디렉터리로 설정합니다.

    이 위치에 우리는 requirements.txt 파일을 생성할 것입니다.

  3. 이 도커 스테이지에서 Poetry를 설치합니다.

  4. 파일 pyproject.tomlpoetry.lock/tmp 디렉터리로 복사합니다.

    ./poetry.lock* (*로 끝나는) 파일을 사용하기 때문에, 파일이 아직 사용가능하지 않더라도 고장나지 않을 것입니다.

  5. requirements.txt 파일을 생성합니다.

  6. 이것이 마지막 스테이지로, 여기에 위치한 모든 것이 마지막 컨테이너 이미지에 포함될 것입니다.

  7. 현재의 워킹 디렉터리를 /code로 설정합니다.

  8. 파일 requirements.txt/code 디렉터리로 복사합니다.

    이 파일은 오직 이전의 도커 스테이지에만 존재하며, 때문에 복사하기 위해서 --from-requirements-stage 옵션이 필요합니다.

  9. 생성된 requirements.txt 파일에 패키지 의존성을 설치합니다.

  10. app 디렉터리를 /code 디렉터리로 복사합니다.

  11. uvicorn 커맨드를 실행하여, app.main에서 불러온 app 객체를 사용하도록 합니다.

버블 숫자를 클릭해 각 줄이 하는 일을 알아볼 수 있습니다.

도커 스테이지Dockefile의 일부로서 나중에 사용하기 위한 파일들을 생성하기 위한 일시적인 컨테이너 이미지로 작동합니다.

첫 스테이지는 오직 Poetry를 설치하고 Poetry의 pyproject.toml 파일로부터 프로젝트 의존성을 위한 requirements.txt를 생성하기 위해 사용됩니다.

requirements.txt 파일은 다음 스테이지에서 pip로 사용될 것입니다.

마지막 컨테이너 이미지에는 오직 마지막 스테이지만 보존됩니다. 이전 스테이지(들)은 버려집니다.

Poetry를 사용할 때 도커 멀티-스테이지 빌드를 사용하는 것이 좋은데, 여러분들의 프로젝트 의존성을 설치하기 위해 마지막 컨테이너 이미지에 오직 requirements.txt 파일만 필요하지, Poetry와 그 의존성은 있을 필요가 없기 때문입니다.

이 다음 (또한 마지막) 스테이지에서 여러분들은 이전에 설명된 것과 비슷한 방식으로 방식으로 이미지를 빌드할 수 있습니다.

TLS 종료 프록시의 배후 - Poetry

이전에 언급한 것과 같이, 만약 여러분이 컨테이너를 Nginx 또는 Traefik과 같은 TLS 종료 프록시 (로드 밸런서) 뒤에서 실행하고 있다면, 커맨드에 --proxy-headers 옵션을 추가합니다:

CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]

요약

컨테이너 시스템(예를 들어 도커쿠버네티스)을 사용하여 모든 배포 개념을 다루는 것은 꽤 간단합니다:

  • HTTPS
  • 구동하기
  • 재시작
  • 복제 (실행 중인 프로세스 개수)
  • 메모리
  • 시작하기 전 단계들

대부분의 경우에서 여러분은 어떤 베이스 이미지도 사용하지 않고 공식 파이썬 도커 이미지에 기반해 처음부터 컨테이너 이미지를 빌드할 것입니다.

Dockerfile에 있는 지시 사항을 순서대로 다루고 도커 캐시를 사용하는 것으로 여러분은 빌드 시간을 최소화할 수 있으며, 이로써 생산성을 최대화할 수 있습니다 (그리고 지루함을 피할 수 있죠) 😎

특별한 경우에는, FastAPI를 위한 공식 도커 이미지를 사용할 수도 있습니다. 🤓