Artículos

Cómo usar django-tenant-schemas para crear una aplicación multiinquilino

Una aplicación multiinquilino, o software multitenencia, es una arquitectura en la que una sola instancia sirve a varios inquilinos. En otras palabras, alojamos la aplicación en un único lugar y todos nuestros clientes, los inquilinos, usan los mismos recursos. Esta arquitectura es una alternativa a la tradicional, donde cada inquilino aloja una instancia dedicada. La siguiente imagen muestra la diferencia entre ambos enfoques.

Gráfico de Lakshay Suri.
Gráfico de Lakshay Suri.

Elegir una arquitectura tradicional o multiinquilino no es sencillo. A veces las reglas de la empresa hacen inviable la opción multiinquilino; por ejemplo, exige alojar la aplicación en sus servidores. Si no es el caso, este enfoque tiene algunas ventajas:

  • Gestión de versiones más sencilla. En un enfoque de un solo inquilino, cada cambio de versión exige actualizar todas las instancias. En uno multiinquilino, solo tienes una instancia que actualizar.
  • Recursos más económicos. Alojar una aplicación multiinquilino suele requerir menos recursos, como CPU y memoria, que alojar la misma aplicación en instancias independientes. Además, una gestión inteligente de los recursos, por ejemplo con un orquestador como Kubernetes, puede ahorrar muchos recursos.
  • Mayor facilidad para compartir información entre inquilinos. A veces hay información pública a la que todos deben acceder. Gestionarla resulta más sencillo con un enfoque multiinquilino que con uno tradicional.

Varios tipos de aplicaciones pueden beneficiarse de una arquitectura multiinquilino. Entre los ejemplos más claros están los productos SaaS, software como servicio.

Bases de datos en aplicaciones multiinquilino

Las aplicaciones multiinquilino deben resolver problemas complejos. La principal característica de esta arquitectura es compartir recursos, lo que presenta desafíos como la distribución de recursos, la seguridad y las personalizaciones. En esta publicación nos centraremos únicamente en el diseño de bases de datos.

En una aplicación multiinquilino se comparten recursos, pero la información de cada inquilino suele ser privada. Debemos garantizar que esté protegida y solo sea accesible para sus miembros. Hay varias formas de implementarlo, pero enumeraremos solo 3.

Múltiples instancias de base de datos

Quizá sea la opción más segura en términos de privacidad y seguridad. Cada inquilino tiene una instancia independiente de base de datos donde se almacena su información. El servidor de aplicaciones debe conectarse a todas y determinar qué base de datos activar para cada usuario según el inquilino al que pertenece.

Algunas ventajas de este enfoque:

  • Los datos son seguros y privados porque se almacenan en bases de datos independientes. Cada inquilino solo accede a su propia base de datos, y la información de los demás permanece privada.
  • Los demás inquilinos nunca se conectan a tu base de datos, lo que mejora el rendimiento. Como mencionamos, distribuir recursos entre inquilinos es un desafío. Tener bases de datos distintas elimina las conexiones a ellas como recurso compartido.

Algunas desventajas:

  • Tener múltiples instancias implica costos más altos. Como mencionamos en la introducción, una de las principales ventajas de la arquitectura multiinquilino es compartir recursos y reducir costos. En este enfoque, la base de datos deja de ser un recurso compartido.
  • Incorporar nuevos inquilinos requiere nuevas conexiones a nuevas bases de datos. Este enfoque exige una conexión independiente entre el servidor de aplicaciones y cada base de datos. Al añadir un inquilino, hay que crear una nueva base de datos y conectarla al servidor. Esta conexión puede causar una interrupción en el servidor de aplicaciones que afecte a los demás inquilinos.
  • La gestión de cada base de datos es independiente. Las tareas de gestión, como los respaldos y vacuum, deben hacerse por instancia.
  • Tener datos compartidos entre inquilinos no es trivial. Todos los datos compartidos deben replicarse en todas las bases de datos.

Una base de datos, un esquema

Esta situación es la opuesta a la anterior. La información de todos los inquilinos se almacena en el mismo esquema de base de datos, con las mismas tablas. Cada tabla tiene un atributo que identifica al inquilino propietario de esa información, por ejemplo una columna tenant_id. El servidor de aplicaciones debe filtrar por inquilino cada vez que se hace una solicitud.

Ventajas de este enfoque:

  • Todos los datos se almacenan en una única instancia. La configuración es idéntica a la de una aplicación independiente, lo que facilita las tareas de gestión de la base de datos, como respaldos y vacuum.
  • Existe una única conexión entre la base de datos y el servidor de aplicaciones. Añadir nuevos inquilinos no exige cambios de configuración y resulta trivial.
  • Compartir datos entre inquilinos es sencillo. Solo debes almacenarlos en una tabla sin la columna tenant_id .

Por otro lado, el enfoque también tiene desventajas:

  • El servidor de aplicaciones siempre debe filtrar por tenant_id antes de acceder a los datos. Si no se aplica el filtro, un usuario puede acceder a información de un inquilino del que no es miembro. Esta vulnerabilidad de privacidad implica que un único error de desarrollo en un componente puede comprometer información sensible.
  • Todo el rendimiento de la aplicación puede verse afectado por un solo inquilino, ya que hay una única instancia y todas las tablas son compartidas. Por ejemplo, si un usuario ejecuta una operación masiva sobre una tabla enorme que requiere bloquearla, quedará bloqueada para todos. La usabilidad de la aplicación puede verse afectada porque un inquilino ejecuta una operación masiva.

Una base de datos, múltiples esquemas

Esta opción combina las anteriores. Tenemos una única instancia de base de datos para almacenar la información de todos los inquilinos. La diferencia con el enfoque anterior es que creamos un esquema separado para cada uno. Cuando el servidor necesita acceder a los datos, activa el esquema según el usuario y su inquilino.

Este enfoque comparte algunas de las ventajas anteriores:

  • Los datos son seguros y privados porque se almacenan en esquemas independientes.
  • Todos los datos se almacenan en una única instancia, lo que reduce los costos, las configuraciones y las tareas de gestión.
  • Añadir nuevos inquilinos solo implica crear nuevos esquemas. La conexión a la base de datos es compartida y no exige cambios de configuración.
  • Compartir datos entre inquilinos es sencillo. Todos los datos compartidos pueden almacenarse en un esquema público accesible para todos.

Sin embargo, este enfoque también tiene limitaciones y desventajas:

  • El rendimiento general puede verse afectado por un solo inquilino. Si un usuario empieza a crear nuevas conexiones a la instancia, todos percibirán lentitud. Sin embargo, el problema de bloqueo mencionado antes no existe porque las tablas se almacenan por separado para cada inquilino.
  • Cambiar la estructura de las tablas exige modificar múltiples esquemas. Es una tarea de gestión que puede resultar algo tediosa y compleja.

Implementar multitenencia en una aplicación Django

Ahora que definimos qué es una aplicación multiinquilino y los distintos enfoques de arquitectura de su base de datos, veamos cómo implementarla en Django.

¿Por qué usar la biblioteca django-tenant-schemas?

El primer problema es que Django no ofrece una forma predeterminada de trabajar con múltiples inquilinos. Según nuestra experiencia con Django, la mejor forma de resolver esta limitación es usar el paquete django-tenant-schemas.

Algunas razones que justifican esta decisión:

  • Es una solución de multitenencia con el enfoque de «una base de datos, múltiples esquemas». Creemos que es el que ofrece más ventajas y puede usarse en más situaciones.
  • Es muy fácil integrarlo con una aplicación Django existente. La solución no afecta a tus aplicaciones. A diferencia de otras alternativas que obligan a modificar todas las aplicaciones, este paquete solo necesita una pequeña configuración para empezar a usarse.
  • La biblioteca resuelve el enrutamiento entre inquilinos. Como mencionamos, hay que activar el esquema correspondiente al inquilino con el que trabaja el usuario. La biblioteca ya implementa una solución que facilita esta tarea.
  • Por último, la biblioteca también permite compartir datos entre inquilinos.

Una aplicación de ejemplo

Veamos cómo convertir fácilmente una aplicación Django en multiinquilino mediante un ejemplo sencillo con django-tenant-schemas. Imagina que creamos un producto para el sector transporte que asigna conductores a envíos.

Al comenzar el proyecto no pensamos que escalaría, así que creamos un único proyecto Django con 3 aplicaciones.

Locations

Aplicación responsable de almacenar todas las ubicaciones. Su modelo principal es Location y se ve así:

from django.db import models

class Location(models.Model):
    name = models.CharField(max_length=255, unique=True)
    address = models.CharField(max_length=255) 
    latitude = models.FloatField()
    longitude = models.FloatField()

Drivers

Aplicación responsable de almacenar todos los conductores. Su modelo principal es Driver y se ve así:

from django.db import models

class Driver(models.Model):
    name = models.CharField(max_length=255, unique=True)

Shipments

Aplicación responsable de almacenar todos los envíos. Su modelo principal es Shipment y se ve así:

from django.db import models

from locations.models import Location
from drivers.models import Driver

class Shipment(models.Model):
     origin = models.ForeignKey(Location, on_delete=models.CASCADE)
     destination = models.ForeignKey(Location, on_delete=models.CASCADE)
     driver = models.ForeignKey(Driver, on_delete=models.CASCADE)
     completion = models.DateTimeField()

Imagina que, después de un tiempo, la solución funciona muy bien, varias empresas quieren usarla y el producto sencillo se convierte en una solución SaaS. Es claramente un caso de uso de arquitectura multiinquilino. Convirtamos esta aplicación sencilla en multiinquilino.

Instalar la biblioteca

El primer paso es instalar el paquete django-tenant-schemas con pip.

pip install django-tenant-schemas

Configurar la aplicación para trabajar con inquilinos

El segundo paso es configurar la aplicación para trabajar con inquilinos. La ventaja del paquete django-tenant-schemas es que solo debemos modificar el archivo de configuración de Django.

Por ahora, supongamos que identificaremos al inquilino a partir de la URL, por ejemplo con subdominios.

Los cambios que debemos implementar en este archivo son:

  1. Cambiar DATABASE_ENGINE:
DATABASES = {
    'default': {
        'ENGINE': 'tenant_schemas.postgresql_backend',
        # ..
    }
}
  1. Añadir tenant_schemas.routers.TenantSyncRouter a DATABASE_ROUTERS:
DATABASE_ROUTERS = (
    'tenant_schemas.routers.TenantSyncRouter',
)
  1. Como suponemos que los inquilinos se identifican por la URL, podemos usar el middleware del paquete para dirigir correctamente las solicitudes a los esquemas.
MIDDLEWARE_CLASSES = (
    'tenant_schemas.middleware.TenantMiddleware',
)
  1. Crear y declarar un modelo de inquilino. En toda aplicación multiinquilino debemos especificar en algún lugar los distintos inquilinos con los que trabajamos. Aquí necesitamos un modelo que herede de_ TenantMixin _y declararlo en el archivo de configuración como TENANT_MODEL. En el ejemplo crearemos una aplicación llamada organizations y un modelo de inquilino llamado Organization.
from django.db import models
from tenant_schemas.models import TenantMixin

class Organization(TenantMixin):
    name = models.CharField(max_length=100)
TENANT_MODEL = "organizations.Organization"
  1. Dividir las aplicaciones en compartidas o de inquilino. Con este paquete, la diferencia entre un modelo compartido y uno de inquilino se define a nivel de aplicación y se declara en el archivo de configuración. Las aplicaciones compartidas se almacenan en el esquema público, accesible para todos los inquilinos; las de inquilino tendrán un esquema independiente. Podemos suponer que las ubicaciones son públicas y gestionar esta aplicación como compartida. En cambio, drivers y shipments almacenan información privada de cada inquilino, así que debemos declararlas como aplicaciones de inquilino. Con estos supuestos, la configuración se ve así:
SHARED_APPS = (
    'tenant_schemas',  # mandatory, should always be before any django app
    'organizations', # you must list the app where your tenant model resides in
    'locations',
    'django.contrib.contenttypes',
)

TENANT_APPS = (
    'django.contrib.contenttypes',
    # your tenant-specific apps
    'drivers',
    'shipments',
)

INSTALLED_APPS = (
    'tenant_schemas',  # mandatory, should always be before any django app

    'organizations',
    'django.contrib.contenttypes',
    'django.contrib.auth',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.admin',
    'locations',
    'drivers',
    'shipments',
)

Después de estos pasos tendrás todo listo para desplegar y ejecutar la aplicación con una arquitectura multiinquilino. Solo debes tener en cuenta las migraciones. El comando migrate predeterminado de Django crea todas las tablas en el esquema público y hace que la aplicación falle al acceder a una aplicación de inquilino. Para desplegarla correctamente, debes usar el comando migrate_schema.

Limitaciones del paquete

Después de trabajar con el paquete, la única limitación que encontramos es que solo funciona con Postgres.

Como mostramos, crear una aplicación multiinquilino en Django no es complejo y te permite implementar una solución SaaS sin problemas. Nuestro ejemplo presenta el uso más básico y sencillo de django-tenant-schemas. El paquete tiene más funciones para crear soluciones más complejas; por ejemplo, puedes personalizar el middleware y elegir el inquilino a partir de algo distinto de la URL. Para más información, consulta la documentación oficial. Esperamos que esta publicación te ayude a comprender mejor qué es una aplicación multiinquilino y cómo crearla en Django.

“Cómo usar django-tenant-schemas para crear una aplicación multiinquilino” de Pablo Grill está bajo la licencia CC BY SA. Los ejemplos de código fuente están bajo la licencia MIT.

Foto de Matthew T. Radar. Gráfico de Lakshay Suri.

Clasificado en Investigación y aprendizaje.

Lecturas relacionadas