Привет, Хабр!
Мульти-тенант (multi-tenancy) — это подход, который позволяет одному экземпляру приложения обслуживать множество клиентов или арендаторов (тенатов). Каждый арендатор изолирован от других, имея возможность кастомизации под свои нужды, при этом основной кодовой базой и инфраструктурой делится между всеми.
Когда применять эту замечательную концепцию? Если говорить простыми словами, то мульти-тенант подход наиболее ценен для SaaS-продуктов, когда одно и то же приложение предоставляется разным клиентам, и каждый клиент работает со своим набором данных. Все это серьезно экономит ресурсы на обслуживание инфраструктуры, тк все изменения вносятся централизованно и мгновенно становятся доступны всем клиентам.
В Django мульти-тенант реализовывается довольно часто и для этого есть библиотека django-multitenant.
Установим и настроим
Установим django-multitenant
через пип:
pip install django-multitenant
После установки пора добавить django_multitenant
в INSTALLED_APPS
проекта Django в settings.py
.
Необходимо обновить настройки БД, используя 'ENGINE': 'django_tenants.postgresql_backend'
, чтобы включить поддержку, к примеру схем PostgreSQL:
DATABASES = {
'default': {
'ENGINE': 'django_tenants.postgresql_backend',
'NAME': 'your_db_name',
'USER': 'your_db_user',
'PASSWORD': 'your_db_password',
'HOST': 'your_db_host',
'PORT': 'your_db_port',
}
}
Нужно также добавить django_tenants
в список установленных приложений:
INSTALLED_APPS = [
...
'django_tenants',
...
]
Работа с тенантами
Для создания тенанта и связанных с ним доменов необходимо определить модели Tenant
и Domain
, как было описано в базовых настройках:
# models.py
from django_tenants.models import TenantMixin, DomainMixin
class Client(TenantMixin):
name = models.CharField(max_length=100)
class Domain(DomainMixin):
pass
После создания моделей, можно программно добавлять новых тенантов и домены:
# добавление нового тенанта
from your_app.models import Client, Domain
tenant = Client(schema_name='new_tenant', name='New Tenant')
tenant.save() # сначала сохраняем тенанта
# добавление домена для тенанта
domain = Domain(domain='newtenant.example.com', tenant=tenant, is_primary=True)
domain.save()
Для применения миграций к схеме конкретного тенанта есть команда migrate_schemas
:
python manage.py migrate_schemas --schema=new_tenant
В некоторых случаях может потребоваться динамическое создание тенантов:
def create_tenant(user):
new_schema_name = generate_schema_name(user)
new_tenant = Client(schema_name=new_schema_name, name=user.company_name)
new_tenant.save()
domain = Domain(domain=f"{new_schema_name}.yourdomain.com", tenant=new_tenant, is_primary=True)
domain.save()
Можно использовать яmiddleware, которые помогают определить, к какому тенанту относится текущий запрос:
MIDDLEWARE = [
'django_tenants.middleware.main.TenantMainMiddleware',
# другие middleware...
]
В django-tenants
есть изоляция static и media файлов между тенантами. Настройка путей для этих файлов производится в settings.py
:
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
# django-tenants для управления файлами
MULTITENANT_RELATIVE_MEDIA_ROOT = '/tenant_media/'
Тесты
django-tenants
предоставляет класс TenantTestCase
, который является подклассом django.test.TestCase
. TenantTestCase
автоматически создает публичного тенанта перед выполнением тестов и удаляет его после:
from django_tenants.test.cases import TenantTestCase
class YourTenantTest(TenantTestCase):
def test_something(self):
# тестовый код
Внутри TenantTestCase
можно создавать дополнительные тенанты для тестирования сценариев, требующих взаимодействия между разными тенантами:
from django_tenants.utils import tenant_context
class MultiTenantTest(TenantTestCase):
def test_multi_tenant_interaction(self):
# создание нового тенанта для теста
new_tenant = self.create_tenant(domain_url='newtenant.test.com', schema_name='newtenant')
# использование контекста тенанта для тестирования взаимодействия
with tenant_context(new_tenant):
# тестовый код
Можно управлять миграциями в тестовых сценариях:
from django_tenants.test.cases import FastTenantTestCase
class YourMigrationTest(FastTenantTestCase):
def test_migration(self):
# тест
Также есть FastTenantTestCase
который более быстрей TenantTestCase
за счет минимизации операций с базой данных.
Можно работать с DNS
В settings.py
проекта можно определить, какие приложения являются общими для всех тенантов SHARED_APPS
и какие приложения уникальны для каждого тенанта TENANT_APPS
:
# settings.py
SHARED_APPS = [
'django_tenants', # обязательно
'your_app', # здесь указывается имя приложения
# другие общие приложения
]
TENANT_APPS = [
'django.contrib.contenttypes',
# приложения специфичные для тенантов
]
INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS]
Для обработки запросов к разным тенантам нужно настроить URL-конфигурацию соответствующим образом, используя django-tenants
URL router:
# urls.py
from django.conf.urls import url
from django_tenants.utils import tenant_urlpatterns
urlpatterns = [
# URL-конфигурации
]
urlpatterns += tenant_urlpatterns([
# URL-конфигурации специфичные для тенантов
])
На стороне DNS для поддоменов, представляющих разные тенанты, необходимо создать соответствующие записи A или CNAME, указывающие на IP-адрес сервера, где размещен Django-проект.
Например, если основной домен example.com
, для тенанта tenant1
будет такая запись:
Type: CNAME
Name:
tenant1.example.com
Value:
example.com.
При создании нового тенанта можно автоматически добавлять доменные записи для него, используя сигналы Django или переопределяя метод save
модели тенанта:
# models.py
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=Client)
def create_domain_for_new_tenant(sender, instance, created, **kwargs):
if created:
Domain.objects.create(domain='{}.example.com'.format(instance.name.lower()), tenant=instance, is_primary=True)
Более подробно с документацией можно ознакомиться здесь.
Подробнее про архитектуру приложений вы можете узнать в рамках онлайн-курсов от практикующих экспертов отрасли. Подробности в каталоге.