Python-микросервисы: Как подружить Django и FastAPI без головной боли
Django и FastAPI являются наиболее популярными фреймворками для разработки web-приложений на Python. Действительно, в случае Django мы имеем отличный инструмент, позволяющий крайне быстро создать полноценное fullstack‑приложение, при этом имея из коробки огромное количество возможностей достаточно простых решений для основных типовых задач. В случае же FastAPI получаем гибкость, легковесность, высокую производительность и асинхронное ядро. А что, если для своего продукта мы хотим использовать лучшее от каждого фреймворка?
Как правило, для любого «продающего» веб‑продукта разрабатывается так называемая админка, с которой будут взаимодействовать внутренние сотрудники для ведения бизнеса. Мы знаем, что у Django админка есть из коробки, но не хотим лепить очередной Django‑монолит, который с течением времени приобретает все негативные качества любого монолита, но также имеет кучу специфических архитектурных особенностей — от синхронной ORM до «Vendor lock‑in» на уровне экосистемы. Но и делать админку кастомно через FastAPI для backend и ЧтоУгодноJS на frontend выглядит избыточным. Решение очевидно — разрабатываем внутреннюю админку на Django, а для всех прочих сервисов используем FastAPI. Что может пойти не так?
На деле такое решение требует реализации многих тонких моментов, игнорирование которых может обернуться если не катастрофой, то значительными трудностями в будущем. В этой статье я приведу некоторые практические рекомендации, которые позволят избежать головной боли и по-настоящему подружат Django и FastAPI так, что вы легко сможете реализовать их синергию в своем продукте!
Вы также можете посмотреть демонстрационный проект и ознакомиться с его исходным кодом: https://gitlab.com/Ergo_Sum/fastapi_and_django_services_lms_demo
Главный принцип: у данных должен быть один владелец
Первый и самый важный принцип такой: каждая таблица должна иметь одного владельца.
Если таблица принадлежит FastAPI-сервису, то:
только он определяет ее схему;
только он хранит миграции;
только он эволюционирует структуру таблицы;
Django может ее читать и даже редактировать записи, но не должен владеть ею на уровне schema management.
В моем демонстрационном проекте это реализовано через разделение по схемам PostgreSQL. user_service владеет схемой users, course_service владеет схемой courses, а enrollment_service — схемой enrollments. Такое разделение делает разделение зон ответственности видимым на уровне самой базы. Когда таблицы всех сервисов лежат в public, границы быстро размываются. К тому же, разделение с помощью БД-схем оставляет возможность легко и просто масштабироваться, банально перенося, скажем, в новую базу только те модели, которые принадлежат одной или другой схеме, не трогая остальные.
Django в этой архитектуре подключается к той же базе, но работает с сервисными таблицами через unmanaged-модели. То есть модели в админке существуют, но для них указано managed = False. Это означает, что Django знает о таблицах, может их читать и отображать, но не пытается создавать, менять или удалять их своими миграциями. Так мы встраиваем Django-админку в микросервисный подход: админка может быть удобным интерфейсом над чужими данными, но не должна становиться владельцем этих данных.
Файл: backoffice/lms_admin/models.py
class UnmanagedModel(models.Model):
class Meta:
abstract = True
managed = False
class Student(UnmanagedModel):
id = models.BigIntegerField(primary_key=True)
full_name = models.CharField(max_length=180)
email = models.EmailField(max_length=180, unique=True)
is_active = models.BooleanField(default=True)
group_id = models.IntegerField()
external_ref = models.CharField(max_length=64, unique=True)
class Meta(UnmanagedModel.Meta):
db_table = 'users"."students'Связи
Вторая важная мысль состоит в том, что между доменами лучше не строить FK на уровне базы. В монолите естественно сделать связь enrollments.student_id к users.students.id, но в микросервисной архитектуре мы хотим изолировать таблицы так, чтобы миграциями каждой из них владел только один сервис. Очевидно, в нашем случае с разделением по схемам (аналогично при разделении по разным БД) между схемами передаются только идентификаторы. При этом внутри своей схемы FK вполне допустимы: например, у студента есть группа, у курса есть категория, у enrollment есть статус.
Файл: services/course_service/app/models.py
class Course(Base):
__tablename__ = "courses"
__table_args__ = {"schema": "courses"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
title: Mapped[str] = mapped_column(String(180))
category_id: Mapped[int] = mapped_column(ForeignKey("courses.course_categories.id"))
Возникает естественный вопрос: не превратится ли такая админка в неудобный интерфейс с голыми student_id и course_id? На практике нет, если немного помочь Django admin. В формах можно использовать обычные choice-поля, чтобы вместо набора чисел оператор видел понятные значения. А в списках можно показывать вычисляемые поля, которые по идентификатору находят человекочитаемое имя. То есть архитектурно мы не строим жесткие связи между доменами, но интерфейс для людей все равно остается нормальным.
Файл: backoffice/lms_admin/forms.py
class EnrollmentAdminForm(forms.ModelForm):
student_id = forms.TypedChoiceField(coerce=int, label="Student")
course_id = forms.TypedChoiceField(coerce=int, label="Course")
status_id = forms.TypedChoiceField(coerce=int, label="Status")Миграции
С FastAPI сервисами все довольно несложно: каждый сервис мигрирует таблицы и данные только в свою схему. Однако, есть одна значительная проблема. Если несколько сервисов используют Alembic в одной базе, по умолчанию они конфликтуют из-за общей таблицы alembic_version. Поэтому каждому сервису нужна своя версия этой таблицы внутри своей схемы. Иначе второй сервис начнет путать миграционную историю первого. Это маленькая деталь, но именно на таких мелочах подобные архитектуры обычно и начинают рассыпаться при первом реальном запуске.
Файл: services/user_service/alembic/env.py
SERVICE_SCHEMA = "users"
VERSION_TABLE = "alembic_version"
context.configure(
connection=connection,
target_metadata=target_metadata,
include_schemas=True,
version_table=VERSION_TABLE,
version_table_schema=SERVICE_SCHEMA,
)
Django при этом мигрирует только собственные служебные таблицы: auth, admin, contenttypes, sessions. Как только Django начинает мигрировать еще и сервисные таблицы, он перестает быть просто backoffice-слоем и начинает незаметно перетягивать зоны ответственности на себя. Тем не менее, админка может иметь и свои собственные таблицы. В таком случае есть три варианта реализации, и все они имеют право на жизнь:
И служебные таблицы, и бизнесовые таблицы админки хранятся в схеме админки.
Служебные таблицы хранятся в схеме public, а бизнесовые - в схеме админки.
И служебные таблицы, и бизнесовые таблицы админки хранятся в схеме public.
Для локальной разработки в проекте есть отдельный migrator-сервис. Он обходит соседние каталоги FastAPI-сервисов, находит у них alembic.ini и последовательно применяет alembic upgrade head. Благодаря этому локальное окружение поднимается одной командой, а разработчику не нужно помнить ручной порядок запуска миграций. Для production это не обязательно единственный правильный вариант, но для демо-стенда, локальной разработки и onboarding-сценариев подход получается очень удобным.
Файл: migrator/run_migrations.py
def discover_alembic_projects() -> list[Path]:
return sorted(path.parent for path in SERVICES_DIR.glob("*/alembic.ini"))subprocess.run(
[sys.executable, "-m", "alembic", "-c", "alembic.ini", "upgrade", "head"],
cwd=service_dir,
env=env,
check=True,
)Вывод
Django и FastAPI отлично уживаются водном продукте, если не пытаться сделать из них «полумонолит», в котором все могут все. Django хорош как быстрый backoffice. FastAPI хорош как слой доменных микросервисов. PostgreSQL‑схемы помогают провести реальную границу владения данными. А unmanaged‑модели позволяют админке работать с сервисными таблицами, не присваивая себе право управлять их жизненным циклом.
Описанные выше рекомендации отлично подходят как для создания нового микросервисного проекта (настроить Django‑админку выйдет дешевле поднятия полноценного внутреннего клиентского сервиса и соответствующего API для него), так и для распила монолита, скорее всего, изначально написанного на Django. Сами «правила» настройки такой связки фреймворков не только позволяют реализовать корректные взаимодействия между приложениями, но и приучает мыслить шире: учитывать варианты масштабирования, отказоустойчивости и общей инфраструктурной гибкости.
А главное, теперь нет чувства, что если в базе случится кризис, то с ним придется очень долго и усердно разбираться. Ведь теперь все разложено по полочкам! Точнее, по схемам.