Запустите веб-сервер разработки Django с помощью Python manage.py:
# python manage.py runserver
По умолчанию сервер запускается на порту 8000 по адресу 127.0.0.1
Модель базы данных назначения
Обновите переменную INSTALLED_APPS в backend/backend/settings.py:
# остальная часть кода ...
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework", # DRF
"assignments", # new app
]
# остальная часть кода ...
Создадим модель бд для объектов назначения
Отредактируйте backend/assignments/models.py:
from django.db import models
class Assignment(models.Model):
first_term = models.DecimalField(
max_digits=5, decimal_places=2, null=False, blank=False
)
second_term = models.DecimalField(
max_digits=5, decimal_places=2, null=False, blank=False
)
# сумма должна быть равна first_term + second_term
# ее значение будет вычислено в Celery
sum = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
Нужно создать и применить миграции:
# python manage.py makemigrations
# python manage.py migrate
Мы будем использовать DRF ModelViewSet для создания CRUD REST API для наших моделей
Добавьте файл serializers.py в каталог backend/assignments:
from rest_framework import serializers
from assignments.models import Assignment
class AssignmentSerializer(serializers.ModelSerializer):
class Meta:
model = Assignment
read_only_fields = ("id", "sum")
fields = ("id", "first_term", "second_term", "sum")
Поля код и сумма, доступны только для чтения
Идентификатор устанавливается автоматически в базе данных
Значение суммы будет вычисляться celery
backend/assignments/views.py:
from django.db import transaction
from rest_framework import viewsets
from rest_framework.exceptions import APIException
from assignments.models import Assignment
from assignments.serializers import AssignmentSerializer
from assignments.tasks import task_execute
class AssignmentViewSet(viewsets.ModelViewSet):
serializer_class = AssignmentSerializer
queryset = Assignment.objects.all()
def perform_create(self, serializer):
try:
with transaction.atomic():
# сохранить экземпляр
instance = serializer.save()
instance.save()
# создание параметров задачи
job_params = {"db_id": instance.id}
# отправить задачу для выполнения в фоновом режиме
transaction.on_commit(lambda: task_execute.delay(job_params))
except Exception as e:
raise APIException(str(e))
ModelViewSet выполняет здесь работу с базовой реализацией CRUD
Мы перезаписываем функцию perform_create для отправки фоновой задачи после создания объекта
Создание объекта и отправка фоновых задач находятся внутри транзакции
Реализуйте функцию task_execute, добавьте tasks.py файл в каталог backend/assignments:
from celery import shared_task
from assignments.models import Assignment
@shared_task()
def task_execute(job_params):
assignment = Assignment.objects.get(pk=job_params["db_id"])
assignment.sum = assignment.first_term + assignment.second_term
assignment.save()
task_execute - общая задача, она будет обнаружена Celery
Принимает один аргумент job_params, который имеет db_id, который является идентификатором объекта присваивания
Мы просто получаем объект по идентификатору, вычисляем сумму и сохраняем объект
Нужно связать конечные точки URL-адресов назначений, добавьте новый файл urls.py в backend/assignments:
from django.urls import re_path
from rest_framework.routers import DefaultRouter
from assignments.views import AssignmentViewSet
router = DefaultRouter()
router.register(r"assignments", AssignmentViewSet, basename="assignments")
assignments_urlpatterns = router.urls
Мы используем DRF DefaultRouter для генерации конечных точек CRUD API.
Добавьте assignments_urlpatterns в main urls.py, отредактируйте файл backend/backend/urls.py:
from django.contrib import admin
from django.urls import path
from assignments.urls import assignments_urlpatterns
urlpatterns = [
path("admin/", admin.site.urls),
]
urlpatterns += assignments_urlpatterns
Настройка Celery
Нам нужно настроить Celery для работы с Django
Добавьте новый файл celery.py в backend/backend directory:
import os
from celery import Celery
from django.conf import settings
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
app = Celery("backend")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
Обновите backend/backend/__init__.py:
from .celery import app as celery_app
__all__ = ("celery_app",)
Нам нужно добавить переменные конфигурации в конце backend/backend/settings.py:
# остальная часть кода ...
# брокер celery и результат
CELERY_BROKER_URL = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND = "redis://localhost:6379/0"
Локальный запуск
Запустите сервер разработки Django в первом терминале:
python manage.py runserver
Во втором терминале, запустите программу Celery worker:
# please run in the backend directory (the same dir as runserver command)
celery -A backend worker --loglevel=info --concurrency 1 -E
Запустите ее во внутреннем каталоге (тот же каталог, что и команда runserver)
Откройте веб-браузер по адресу 127.0.0.1:8000/assignments
Создайте новый объект Assignment, нажав кнопку POST
После создания объекта сумма должна быть равна нулю
Нажмите ПОЛУЧИТЬ в правом верхнем углу, чтобы получить список всех назначений
Вы должны увидеть вычисленное значение суммы
Это было вычислено в фоновом режиме с помощью Celery framework
Мы используем здесь очень простой пример, который выполняется быстро
В реальном мире выполнение фоновой задачи может занять даже часы - зависит от проекта
Создайте Dockerfile
Пришло время создать Dockerfile для сервера и worker
Сервер и worker будут работать в разных контейнерах, но у них будет один и тот же Dockerfile
Добавьте новые каталоги:
docker
docker/backend
Добавьте новый файл Dockerfile в docker/backend:
FROM python:3.8.15-alpine
RUN apk update && apk add python3-dev gcc libc-dev
WORKDIR /app
RUN pip install --upgrade pip
RUN pip install gunicorn
ADD ./requirements.txt /app/
RUN pip install -r requirements.txt
ADD ./backend /app/backend
ADD ./docker /app/docker
RUN chmod +x /app/docker/backend/server-entrypoint.sh
RUN chmod +x /app/docker/backend/worker-entrypoint.sh
строка 1: мы используем python: 3.8.15-alpine image в качестве базы
строка 3: мы устанавливаем библиотеки разработки gcc и python для установки и сборки новых пакетов
строка 5: добавляем каталог приложения и устанавливаем его как рабочий каталог
строка 6: копируем requirements.txt файл в изображение
строки 8-10: обновляем pip, устанавливаем gunicorn и необходимые пакеты
строки 12-13: скопируйте каталоги серверной части и docker в образ docker
строки 15-16: добавьте права на выполнение скриптам entrypoint
Скрипты Entrypoint
Добавим скрипт, который запустит сервер gunicorn
Добавьте server-entrypoint.sh файл в директорию docker/backend:
#!/bin/sh
until cd /app/backend
do
echo "Waiting for server volume..."
done
until python manage.py migrate
do
echo "Waiting for db to be ready..."
sleep 2
done
python manage.py collectstatic --noinput
# python manage.py createsuperuser --noinput
gunicorn backend.wsgi --bind 0.0.0.0:8000 --workers 4 --threads 4
# debug
#python manage.py runserver 0.0.0.0:8000
Пояснения к server-entrypoint.sh скрипту:
строки 3-6: ждем, пока будет готов каталог /app/backend.
строки 9-13: ждем, пока база данных будет готова, и запускаем миграцию.
строка 16: собираем статические файлы.
строка 20: запуск сервера gunicorn.
строка 23: Команда запуска сервера разработки(При использовании комментируйте строку 20)
У воркера будет отдельный скрипт в worker-entrypoint.sh:
#!/bin/sh
until cd /app/backend
do
echo "Waiting for server volume..."
done
# run a worker
celery -A backend worker --loglevel=info --concurrency 1 -E
Скрипт запускает только одного воркера
Вы можете увеличить количество воркеров, изменив параметр --concurrency
Конфигурация Nginx
Сервер Nginx нужен для прокси-запросов к серверу gunicron (Django) и для обслуживания статических файлов
Будем использовать образ docker по умолчанию (nginx: 1.23-alpine)
Нам нужно предоставить файл конфигурации для настройки работы сервера
Сервер будет отправлять все запросы / и /admin в @proxy_api, являющимся gunicorn, обслуживающим Django
Запросы /django_static/ обслуживаются /app/backend/django_static/ - путь Django сбора статических файлов
Создаем docker-compose
У нас готов Dockerfile для сервера и воркера
Конфигурация Nginx ожидает запросов
строки 4-11 - это определение контейнера nginx.
В нем используется стандартный nginx: 1.23-alpine image.
Копируем файл конфигурации в строке 10.
Мы добавляем static_volume в строку 11.
Тот же static_volume будет смонтирован для серверного контейнера.
Все статические файлы будут отправлены туда.
За пределами docker доступен порт 80, к нему можно подключиться из внешнего мира.
строки 12-31 - это наш сервер Django.
Он создан с помощью /docker/backend/Dockerfile (строка 16).
В строке 19 установлен static_volume .
Он предоставляет порт 8000 - к нему можно получить доступ только внутри docker-compose.
У нас есть переменные среды, установленные в строках 22-31.
Контейнер выполнит server-netrypoint.sh скрипт (строка 17).
строки 32-52 - это рабочий контейнер.
Он не предоставляет никаких портов.
Это зависит от контейнеров redis и сервера (строки 50-52).
строки 53-57 - это контейнер Redis.
Он предоставляет внутренний порт 6379 (недоступен извне из docker-compose).
строки 58-68 - это база данных Postgres.
Он предоставляет внутренний порт 5432.
В строках 63-66 есть переменные окружения для инициализации базы данных при запуске.
Все файлы базы данных хранятся в томе postgres_data.
строки 70-72 - определение томов, используемых в docker-compose.
Мы почти готовы начать использовать docker-compose
Нам нужно обновить конфигурацию Django для чтения переменных среды
Отредактируйте backend/backend/settings.py:
import os
#
# rest of the code ...
#
# set variables
SECRET_KEY = os.environ.get(
"SECRET_KEY", "django-insecure-6hdy-)5o6k6it_6x%s#u0#guc3(au!=v%%qb674(upu6rrht7b"
)
DEBUG = os.environ.get("DEBUG", True)
ALLOWED_HOSTS = ["127.0.0.1", "0.0.0.0"]
if os.environ.get("ALLOWED_HOSTS") is not None:
try:
ALLOWED_HOSTS += os.environ.get("ALLOWED_HOSTS").split(",")
except Exception as e:
print("Cant set ALLOWED_HOSTS, using default instead")
#
# rest of the code ...
#
# set database, it can be set to SQLite or Postgres
DB_SQLITE = "sqlite"
DB_POSTGRESQL = "postgresql"
DATABASES_ALL = {
DB_SQLITE: {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
},
DB_POSTGRESQL: {
"ENGINE": "django.db.backends.postgresql",
"HOST": os.environ.get("POSTGRES_HOST", "localhost"),
"NAME": os.environ.get("POSTGRES_NAME", "postgres"),
"USER": os.environ.get("POSTGRES_USER", "postgres"),
"PASSWORD": os.environ.get("POSTGRES_PASSWORD", "postgres"),
"PORT": int(os.environ.get("POSTGRES_PORT", "5432")),
},
}
DATABASES = {"default": DATABASES_ALL[os.environ.get("DJANGO_DB", DB_SQLITE)]}
#
# rest of the code ...
#
# set static URL address and path where to store static files
STATIC_URL = "/django_static/"
STATIC_ROOT = BASE_DIR / "django_static"
#
# rest of the code ...
#
# celery broker and result
CELERY_BROKER_URL = os.environ.get("BROKER_URL", "redis://localhost:6379/0")
CELERY_RESULT_BACKEND = os.environ.get("RESULT_BACKEND", "redis://localhost:6379/0")