Dockerizing Celery and FastAPI

Hyeseong·2022년 5월 16일
0

개요

1. 도커 컴포즈란? 사용 이유?
2. FastAPI, Postgres, Redis, Celery를 생성 및 관리를 위한 컴포즈 
3. 도커와 컴포즈를 이용해서 개발 생산성 높이기

도커 컴포즈

Docker Compose 는 다중 컨테이너 Docker 애플리케이션을 정의하고 실행하는 데 사용되는 도구입니다.
YAML 파일을 사용하여 애플리케이션의 서비스를 구성하고 단일 명령으로 모든 컨테이너에 대한 생성 및 시작 프로세스를 수행합니다.

도커 명령어를 사용하여 redis 컨테이너를 구성하는 방법에 대해서 이미 살펴 보았습니다.

$ docker run -p 6379:6379 --name some-redis -d redis

이번 챕터에서는 전체 인프라를 컨테이너화해서 개발을 단순화 해볼게요.
그 전에 컨테이너화를 하는 이유는 무엇일까요?

  1. 각 프로세스(예: Uvicorn/FastAPI, Celery worker, Celery beat, Flower, Redis, Postgres 등)를 각각 다른 터미널 창에서 수동으로 실행하는 대신 서비스를 컨테이너화한 후 Docker Compose를 사용하여 단일 명령을 사용하여 모든 컨테이너를 기동 할 수 있습니다.

  2. 컴포즈는 configuration을 단순화 할 수 있습니다. Celery config는 현재 FastAPI 앱 config에 묶여있습니다. 현재 이러한 구성은 최선은 아닙니다. 컴포즈를 이용하면, YAML 파일에서 FastAPI와 Celery를 다른 configuration 쉽게 생성 할 수 있습니다.

  3. isolation, reproduction, portable한 개발 환경을 쉽게 구성 할 수 있습니다. 그래서 가상환경에 이것저것 잡다하게 설치할 필요도 없습니다. 그리고 local OS에 Postgres와 Redis를 바로 설치 하지 않아도 됩니다.

Config 파일 구조

전체 workflow를 이해하기 위해서 config file 구조부터 살펴 볼게요.

── alembic
├── alembic.ini
├── compose
│   └── local
│       └── fastapi
│           ├── Dockerfile
│           ├── celery
│           │   ├── beat
│           │   │   └── start
│           │   ├── flower
│           │   │   └── start
│           │   └── worker
│           │       └── start
│           ├── entrypoint
│           └── start
├── docker-compose.yml
├── main.py
├── project
├── requirements.txt

지금 바로 위와 같이 폴더와 파일 구조를 만들지 마세요. 진행 하면서 조금씩 만들어 갈ㄹ게요.

컴포즈를 사용하면 docker-compose.yml 파일의 선언적 구문을 사용하여 원하는 환경의 상태를 명세 할 수 있습니다.

컴포즈 폴더에는 각 환경에 대한 configuration files, 쉘 스크립트와 관련 도커 파일이 들어 있습니다.

Application Services

프로젝트 디렉토리에 docker-compose.yml을 생성 할 게요.

version: '3.8'

services:
  web:
    build:
      context: .
      dockerfile: ./compose/local/fastapi/Dockerfile
    image: fastapi_celery_example_web
    # '/start'는 서비스를 기동하기 위한 쉘 스크립트입니다. 
    command: /start
    # 호스트에 있는 파일과 폴더를 컨테이너와 매핑하기 위해서 볼륨 구성을 하게 됩니다. 
    # 만약 호스트에서 코드가 변경되면 컨테이너의 코드 역시 변경되어 반영 됩니다. 
    volumes:
      - .:/app
    ports:
      - 8010:8000
    env_file:
      - .env/.dev-sample
    depends_on:
      - redis
      - db

  db:
    image: postgres:13-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      - POSTGRES_DB=fastapi_celery
      - POSTGRES_USER=fastapi_celery
      - POSTGRES_PASSWORD=fastapi_celery

  redis:
    image: redis:6-alpine

  celery_worker:
    build:
      context: .
      dockerfile: ./compose/local/fastapi/Dockerfile
    image: fastapi_celery_example_celery_worker
    command: /start-celeryworker
    volumes:
      - .:/app
    env_file:
      - .env/.dev-sample
    depends_on:
      - redis
      - db

  celery_beat:
    build:
      context: .
      dockerfile: ./compose/local/fastapi/Dockerfile
    image: fastapi_celery_example_celery_beat
    command: /start-celerybeat
    volumes:
      - .:/app
    env_file:
      - .env/.dev-sample
    depends_on:
      - redis
      - db

  flower:
    build:
      context: .
      dockerfile: ./compose/local/fastapi/Dockerfile
    image: fastapi_celery_example_celery_flower
    command: /start-flower
    volumes:
      - .:/app
    env_file:
      - .env/.dev-sample
    ports:
      - 5557:5555
    depends_on:
      - redis
      - db

volumes:
  postgres_data:

위 docker-comopse.yml파일에서 6가지 서비스를 명세 했습니다.

  1. web : FastAPI 서버
  2. db : Postgres 서버
  3. redis : Redis 서비스(Celery - message broker와 result backend로 사용)
  4. celery_worker : Celery Worker Process
  5. celery_beat : Celery Beat Proecess(스케쥴링 작업)
  6. flower : Celery Dashboard

web, celery_worker, celery_beat, and flower services는 모두 동일한 Dockerfile에 명세 됩니다.

환경 변수

프로젝트 루트에 환경 변수를 저장할 새로운 폴더 .env라는 폴더를 만들게요. 이후 .dev-sample라는 파일을 아래와 같이 생성합니다.

FASTAPI_CONFIG=development
DATABASE_URL=postgresql://fastapi_celery:fastapi_celery@db/fastapi_celery
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0

Dockerfile

아래와 같이 프로젝트 루트에 파일과 폴더를 만들게요

└── compose
    └── local
        └── fastapi
            └── Dockerfile

Dockerfile을 아래와 같이 명세합니다.

FROM python:3.9-slim-buster

ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1

RUN apt-get update \
  # dependencies for building Python packages
  && apt-get install -y build-essential \
  # psycopg2 dependencies
  && apt-get install -y libpq-dev \
  # Additional dependencies
  && apt-get install -y telnet netcat \
  # cleaning up unused files
  && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
  && rm -rf /var/lib/apt/lists/*

# 라이브러리와 패키가 캐쉬되어 설치 되도록 아래와 같이 명세하여 줍니다. 
COPY ./requirements.txt /requirements.txt
RUN pip install -r /requirements.txt

COPY ./compose/local/fastapi/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint

COPY ./compose/local/fastapi/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start

COPY ./compose/local/fastapi/celery/worker/start /start-celeryworker
RUN sed -i 's/\r$//g' /start-celeryworker
RUN chmod +x /start-celeryworker

COPY ./compose/local/fastapi/celery/beat/start /start-celerybeat
RUN sed -i 's/\r$//g' /start-celerybeat
RUN chmod +x /start-celerybeat

COPY ./compose/local/fastapi/celery/flower/start /start-flower
RUN sed -i 's/\r$//g' /start-flower
RUN chmod +x /start-flower

WORKDIR /app

ENTRYPOINT ["/entrypoint"]

Dockerfile은 이미지를 빌드하는 데 필요한 명령이 명세된 텍스트 파일입니다.

  1. RUN sed -i 's/\r$//g' /entrypoint 명령어는 Windows -> Unix 라인엔딩으로 2번째 매개변수(파일)를 지정하여 파일안에 모든 라인엔딩을 변경시키게 됩니다.
  2. 각각의 다른 service의 start shell script를 이미지의 root 디렉토리에 복사하도록 COPY 명령어를 사용하였습니다.
  3. WORKDIR 키워드를 통해서 소스코드들이 /app 디렉토리에 저장되도록 지정하였습니다.

Entrypoint

docker-compose.yml 파일에서 depends_on keyword는 redis, db service가 up 되기 전까지는web service는 start되지 않습니다. 그렇지만 만약 db 컨테이너가 up된 상태라고 하더라도 connection 상태가 준비된것 역시 의미하지 않는데요. web service를 돌리기 전 entrypoint라고 불리는 쉘스크립트를 통해서 db와의 연결 상태를 확실하게 만들어 보겠습니다.

compose/local/fastapi/entrypoint

#!/bin/bash

# if any of the commands in your code fails for any reason, the entire script fails
set -o errexit
# fail exit if one of your pipe command fails
set -o pipefail
# exits if any of your variables is not set
set -o nounset

postgres_ready() {
python << END
import sys

import psycopg2
import urllib.parse as urlparse
import os

url = urlparse.urlparse(os.environ['DATABASE_URL'])
dbname = url.path[1:]
user = url.username
password = url.password
host = url.hostname
port = url.port

try:
    psycopg2.connect(
        dbname=dbname,
        user=user,
        password=password,
        host=host,
        port=port
    )
except psycopg2.OperationalError:
    sys.exit(-1)
sys.exit(0)

END
}
until postgres_ready; do
  >&2 echo 'Waiting for PostgreSQL to become available...'
  sleep 1
done
>&2 echo 'PostgreSQL is available'

exec "$@"

start script

start script를 추가 할게요.
"compose/local/fastapi"폴더를 아래와 같이 구조화할 게요.

└── fastapi
    ├── Dockerfile
    ├── celery
    │   ├── beat
    │   │   └── start
    │   ├── flower
    │   │   └── start
    │   └── worker
    │       └── start
    ├── entrypoint
    └── start

4개의 각 start script폴더를 업데이트할 게요.

compose/local/fastapi/start:

#!/bin/bash

set -o errexit
set -o pipefail
set -o nounset

alembic upgrade head
uvicorn main:app --reload --reload-dir project --host 0.0.0.0

compose/local/fastapi/celery/beat/start:

#!/bin/bash

set -o errexit
set -o nounset

rm -f './celerybeat.pid'
celery -A main.celery beat -l info

compose/local/fastapi/celery/worker/start:

#!/bin/bash

set -o errexit
set -o nounset

celery -A main.celery worker --loglevel=info

compose/local/fastapi/celery/flower/start:

#!/bin/bash

set -o errexit
set -o nounset

worker_ready() {
    celery -A main.celery inspect ping
}

until worker_ready; do
  >&2 echo 'Celery workers not available'
  sleep 1
done
>&2 echo 'Celery workers is available'

celery flower \
    --app=main.celery \
    --broker="${CELERY_BROKER_URL}"

마지막 스크립트에서 Celery가 준비되지 않은 경우 flower 실행을 지연시키도록 스크립트를 짜두었습니다.

기본 Workflow


config가 완료되었으면, 전체 워크플로우 이해를 위해서 전체가 어떻게 동작하는지 볼게요.

Postgres database에 연결하기 위해서 psycopg2-binary를 requirements.txt파일에 추가합니다.

psycopg2-binary==2.9.1

가상환경에 deactivate하고 삭제해 줍니다. 그리고 이미지 빌드를 해줍니다.

$ docker-compose build

이미지 빌드가 되었다면, detached mode로 컨테이너를 기동할 게요.

depends_on option에 정의된 순서에 따라서 컨테이너들이 기동 됩니다.
1. 우선 redis와 db 컨테이너가 먼저
2. 그리고 나서 web, celery_worker, celery_beat 그리고 flower 컨테이너

일단 컨테이너들이 up이 되면, entrypoint script가 실행되고 Postgres 상태도 up이 되고 그리고 각각의 start scripts들이 실행 될거에요. db마이그레이션역시 진행되고 개발 서버가 기동되게 됩니다. 그럼 결국 FastAPI app이 available한 상태로 됩니다.

Troubleshooting

문제 발생시 아래 명령어를 통해서 로그를 확인해 볼게요.

$ docker-compose logs -f

문제가 발생된 부분을 수정하였다면 이미지를 다시 빌드하고 컨테이너 기동을 다시 합니다.

컴포즈 명령어

기동중인 특정 컨테이너의 쉘 모드로 진입하기 위해서는 아래 명령어를 사용합니다.

$ docker-compose exec <service-name> bash
$ docker-compose run --rm web bash

위 --rm 옵션을 통해서 bash shell에서 빠져 나오게 된 이후 컨테이너를 삭제할 것을 도커에게 명령하게 됩니다.

테스트

기동중인 컨테이너의 파이썬 쉘 스크립트로 진입하는 명령어를 테스트해 봅니다.

$ docker-compose exec web python

아래 코드를 실행 합니다.

>>> from main import app
>>> from project.users.tasks import divide
>>>
>>> divide.delay(1, 2)
<AsyncResult: a0a8a1eb-db73-4132-b2ae-7b0724af8af3>

새로운 터미널 창을 열고 프로젝트 디렉터리로 이동한 다음 Celery worker의 로그기록을 봅니다.

$ docker-compose logs celery_worker

아래와 같은 유사한 내용이 표시 되어야 합니다.

celery_worker_1  | [2021-05-16 19:14:05,941: INFO/MainProcess] Task project.users.tasks.divide[a0a8a1eb-db73-4132-b2ae-7b0724af8af3] received
celery_worker_1  | [2021-05-16 19:14:10,954: INFO/ForkPoolWorker-8] Task project.users.tasks.divide[a0a8a1eb-db73-4132-b2ae-7b0724af8af3] succeeded in 5.0098771999983s: 0.5

첫번째 창을 종료합니다.
redis 서비스의 쉘로 진입합니다.

$ docker-compose exec redis sh

redis 컨테이너에서는 bash 사용이 불가능하므로 sh를 사용합니다.

위 task ID를 사용하여 task result를 바로 Redis로부터 확인해 볼게요.

$ redis-cli
127.0.0.1:6379> MGET celery-task-meta-1825c77a-bc36-46cc-a851-aed62b1f56ec

1) "{\"status\": \"SUCCESS\", \"result\": 0.5, \"traceback\": null, \"children\": [], \"date_done\": \"2022-05-16T19:14:10.948794\", \"task_id\": \"a0a8a1eb-db73-4132-b2ae-7b0724af8af3\"}"

Flower Dashboard에서 위 결과를 확인 할 수 있어야합니다.

결론

Docker와 Docker Compose를 이용하여 FastAPI, Postgres, Redis, Celery 컨테이너를 어떻게 기동하는지 살펴봤는데요.

최종적으로 프로젝트 디렉토리 구조는 아래와 같이 될 것입니다.

├── .env
│   └── .dev-sample
├── alembic
│   ├── README
│   ├── env.py
│   ├── script.py.mako
│   └── versions
│       ├── 245722b915bf_.py
│       └── 7a852cb80b09_.py
├── alembic.ini
├── celerybeat-schedule
├── compose
│   └── local
│       └── fastapi
│           ├── Dockerfile
│           ├── celery
│           │   ├── beat
│           │   │   └── start
│           │   ├── flower
│           │   │   └── start
│           │   └── worker
│           │       └── start
│           ├── entrypoint
│           └── start
├── docker-compose.yml
├── main.py
├── project
│   ├── __init__.py
│   ├── celery_utils.py
│   ├── config.py
│   ├── database.py
│   └── users
│       ├── __init__.py
│       ├── models.py
│       └── tasks.py
└── requirements.txt
profile
어제보다 오늘 그리고 오늘 보다 내일...

1개의 댓글

comment-user-thumbnail
2022년 11월 6일
답글 달기