Django es un marco popular que puede seleccionar para desarrollar una aplicación para su empresa. Pero, ¿qué sucede si desea crear una aplicación SaaS que utilizarán varios clientes? ¿Qué arquitectura deberías elegir? Veamos cómo se puede abordar esta tarea.
El enfoque más sencillo es crear una instancia separada para cada cliente que tenga. Digamos que tenemos una aplicación Django y una base de datos. Luego, para cada cliente, necesitamos ejecutar su propia base de datos y su propia instancia de aplicación. Eso significa que cada instancia de aplicación tiene un solo inquilino.
Este enfoque es sencillo de implementar: simplemente debe iniciar una nueva instancia de cada servicio que tenga. Pero al mismo tiempo puede causar un problema: cada cliente aumentará significativamente el coste de la infraestructura. Puede que no sea un gran problema si planea tener sólo unos pocos clientes o si cada instancia es pequeña.
Sin embargo, supongamos que estamos construyendo una gran empresa que proporciona un mensajero corporativo a 100.000 organizaciones. ¡Imagínese lo caro que puede resultar duplicar toda la infraestructura para cada nuevo cliente! Y, cuando necesitamos actualizar la versión de la aplicación, debemos implementarla para cada cliente, por lo que la implementación también se ralentizará .
Existe otro enfoque que puede ayudar en un escenario en el que tenemos muchos clientes para la aplicación: una arquitectura multiinquilino. Significa que tenemos varios clientes, a los que llamamos inquilinos , pero todos usan solo una instancia de la aplicación.
Si bien esta arquitectura resuelve el problema del alto costo de las instancias dedicadas para cada cliente, introduce un nuevo problema: ¿cómo podemos estar seguros de que los datos del cliente están aislados de forma segura de otros clientes?
Discutiremos los siguientes enfoques:
Uso de una base de datos compartida y un esquema de base de datos compartida : podemos identificar qué inquilino posee los datos mediante la clave externa que debemos agregar a cada tabla de la base de datos.
Usar una base de datos compartida, pero esquemas de base de datos separados : de esta manera, no necesitaremos mantener múltiples instancias de bases de datos, pero obtendremos un buen nivel de aislamiento de datos de los inquilinos.
Uso de bases de datos separadas : parece similar al ejemplo de un solo inquilino, pero no será el mismo, ya que seguiremos usando una instancia de aplicación compartida y seleccionaremos qué base de datos usar verificando el inquilino.
Profundicemos en estas ideas y veamos cómo integrarlas con la aplicación Django.
Esta opción puede ser la primera que se le ocurra: agregar una ForeignKey a las tablas y utilizarla para seleccionar los datos adecuados para cada inquilino. Sin embargo, tiene una gran desventaja: los datos de los inquilinos no están aislados en absoluto, por lo que un pequeño error de programación puede ser suficiente para filtrar los datos de los inquilinos al cliente equivocado.
Tomemos un ejemplo de estructura de base de datos de la documentación de Django :
from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
Necesitaremos identificar qué registros pertenecen a qué inquilino. Entonces, necesitamos agregar una tabla Tenant
y una clave externa en cada tabla existente:
class Tenant(models.Model): name = models.CharField(max_length=200) class Question(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
Para simplificar un poco el código, podemos crear un modelo base abstracto que se reutilizará en cada uno de los demás modelos que creemos.
class Tenant(models.Model): name = models.CharField(max_length=200) class BaseModel(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) class Meta: abstract = True class Question(BaseModel): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(BaseModel): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
Como puede ver, aquí existen al menos dos riesgos importantes: un desarrollador puede olvidarse de agregar un campo de inquilino al nuevo modelo, o un desarrollador puede olvidarse de usar este campo mientras filtra los datos.
El código fuente de este ejemplo se puede encontrar en GitHub: https://github.com/bp72/django-multitenancy-examples/tree/main/01_shared_database_shared_schema .
Teniendo en cuenta los riesgos del esquema compartido, consideremos otra opción: la base de datos seguirá siendo compartida, pero crearemos un esquema dedicado para cada inquilino. Para la implementación, podemos consultar una biblioteca popular django-tenants ( documentación ).
Agreguemos django-tenants
a nuestro pequeño proyecto (los pasos de instalación oficiales se pueden encontrar aquí ).
El primer paso es la instalación de la biblioteca mediante pip
:
pip install django-tenants
Cambiar los modelos: el modelo Tenant
ahora estará en una aplicación separada. Los modelos de Question
y Choice
ya no tendrán conexión con el inquilino. Como los datos de diferentes inquilinos estarán en esquemas separados, ya no necesitaremos vincular los registros individuales con las filas de los inquilinos.
El archivo inquilinos/modelos.py
from django.db import models from django_tenants.models import TenantMixin, DomainMixin class Tenant(TenantMixin): name = models.CharField(max_length=200) # default true, schema will be automatically created and synced when it is saved auto_create_schema = True class Domain(DomainMixin): # a required table for django-tenants too ...
El archivo encuestas/modelos.py
from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
¡Observe que Pregunta y Elección ya no tienen una clave externa para el Inquilino!
La otra cosa que se cambió es que el inquilino ahora está en una aplicación separada: no solo sirve para separar los dominios, sino que también es importante ya que necesitaremos almacenar la tabla tenants
en el esquema compartido y se crearán tablas polls
para cada inquilino. esquema.
Realice cambios en el archivo settings.py
para admitir múltiples esquemas e inquilinos:
DATABASES = { 'default': { 'ENGINE': 'django_tenants.postgresql_backend', # .. } } DATABASE_ROUTERS = ( 'django_tenants.routers.TenantSyncRouter', ) MIDDLEWARE = ( 'django_tenants.middleware.main.TenantMainMiddleware', #... ) TEMPLATES = [ { #... 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', #... ], }, }, ] SHARED_APPS = ( 'django_tenants', # mandatory 'tenants', # you must list the app where your tenant model resides in 'django.contrib.contenttypes', # everything below here is optional 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.admin', ) TENANT_APPS = ( # your tenant-specific apps 'polls', ) INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS] TENANT_MODEL = "tenants.Tenant" TENANT_DOMAIN_MODEL = "tenants.Domain"
A continuación, creemos y apliquemos las migraciones:
python manage.py makemigrations
python manage.py migrate_schemas --shared
Como resultado, veremos que se creará el esquema público y contendrá solo tablas compartidas.
Necesitaremos crear un inquilino predeterminado para el esquema public
:
python manage.py create_tenant --domain-domain=default.com --schema_name=public --name=default_tenant
Establezca is_primary
en True
si se le solicita.
Y luego, podemos empezar a crear los inquilinos reales del servicio:
python manage.py create_tenant --domain-domain=tenant1.com --schema_name=tenant1 --name=tenant_1 python manage.py create_tenant --domain-domain=tenant2.com --schema_name=tenant2 --name=tenant_2
Observe que ahora hay 2 esquemas más en la base de datos que contienen tablas polls
:
Ahora, obtendrá las preguntas y opciones de diferentes esquemas cuando llame a las API en los dominios que configuró para los inquilinos. ¡Todo listo!
Aunque la configuración parece más complicada y tal vez incluso más difícil si migras la aplicación existente, el enfoque en sí todavía tiene muchas ventajas, como la seguridad de los datos.
El código del ejemplo se puede encontrar aquí .
El último enfoque que discutiremos hoy es ir aún más allá y tener bases de datos separadas para los inquilinos.
Esta vez, tendremos algunas bases de datos:
Almacenaremos los datos compartidos, como la asignación del inquilino a los nombres de las bases de datos en default_db
y crearemos una base de datos separada para cada inquilino.
Luego necesitaremos configurar la configuración de la base de datos en settings.py:
DATABASES = { 'default': { 'NAME': 'default_db', ... }, 'tenant_1': { 'NAME': 'tenant_1', ... }, 'tenant_2': { 'NAME': 'tenant_2', ... }, }
Y ahora podremos obtener los datos de cada inquilino llamando using
el método QuerySet:
Questions.objects.using('tenant_1')…
La desventaja del método es que necesitarás aplicar todas las migraciones en cada base de datos usando:
python manage.py migrate --database=tenant_1
También puede ser menos conveniente crear una nueva base de datos para cada inquilino, en comparación con el uso de django-tenants
o simplemente usar una clave externa como en el enfoque de esquema compartido.
Por otro lado, el aislamiento de los datos del inquilino es realmente bueno: las bases de datos se pueden separar físicamente. Otra ventaja es que no estaremos limitados a usar solo Postgresql como lo requieren django-tenants
, podemos seleccionar cualquier motor que se adapte a nuestras necesidades.
Puede encontrar más información sobre el tema de múltiples bases de datos en la documentación de Django.
| Inquilino único | MT con esquema compartido | MT con esquema separado | MT con bases de datos separadas |
---|---|---|---|---|
Aislamiento de datos | ✅Alto | ❌Más bajo | ✅Alto | ✅Alto |
Riesgo de fuga de datos accidentalmente | ✅Bajo | ❌Alto | ✅Bajo | ✅Bajo |
Costo de infraestructura | ❌Más alto con cada inquilino | ✅Bajar | ✅Bajar | ✅❌ Menor que el de un solo inquilino |
Velocidad de implementación | ❌Bajar con cada inquilino | ✅ | ✅❌ Las migraciones serán más lentas ya que deberán ejecutarse para cada esquema | ✅❌ Las migraciones serán más lentas ya que deben ejecutarse para cada base de datos. |
Fácil de implementar | ✅ | ❌ Requiere muchos cambios si el servicio ya se implementó como una aplicación de inquilino único | ✅ | ✅ |
Para resumir todo lo anterior, parece que no existe una solución mágica para el problema, cada enfoque tiene sus pros y sus contras, por lo que depende de los desarrolladores decidir qué compensación pueden tener.
Las bases de datos separadas proporcionan el mejor aislamiento para los datos del inquilino y son fáciles de implementar; sin embargo, el mantenimiento le cuesta más: n base de datos para actualizar, el número de conexiones de base de datos es mayor.
Una base de datos compartida con un esquema separado es un poco compleja de implementar y puede tener algunos problemas con la migración.
El inquilino único es el más sencillo de implementar, pero le cuesta por el consumo excesivo de recursos, ya que tiene una copia completa de su servicio por inquilino.