Multi Tenants

¿Qué es y como implementarlo?

¿Qué es?

Multi-tenant es una arquitectura que permite a una instancia única de software proveer servicio a varios clientes.

 

Cada cliente es considerado un Tenant.

 

Cada tenant puede personalizar ciertas partes de la aplicación (colores de la interfaz por ejemplo) pero no puede personalizar el código.

¿Pros?

  • Economía en el desarrollo y mantenimiento (los costos son distribuidos entre todos los clientes "tenants")

 

  • Facilidad en las actualizaciones (solo es necesario actualizar una instancia del software).

 

  • Seguridad de la información de cada cliente

 

  • Mejor aprovechamiento de los recursos de los servidores

¿Contras?

  • Dificultad para desarrollar características especificas para un cliente.

 

  • Único punto de falla (la aplicación falla para todos los clientes)

Arquitecturas

Compartida (Shared)

Separada (Isolated)

Híbrida (Semi-Isolated)

Pero primero

¿Qué es un Schema?

Actúa como nombre de espacio (Postgres).

 

Añade una capa adicional de organización/separación a la base de datos.

(Database -> Schema -> Tabla)

 

*Cuando no se especifica un schema en la conexión or consulta, solo se accede al schema publico*

Compartida (Shared)

Una Base de datos - Un schema

Pros

  • Capa de datos fácil de construir
  • Todos los usuarios usan el mismo dominio

 

Contras

  • Costoso (Tamaño de la base de datos y cantidad de peticiones a la db por cada tenant)

Separada (Isolated)

Cada tenant tiene su propia base de datos

Pros

  • Mayor seguridad
  • Facilidad de recuperación en caso de perdida de datos
  • No consume demasiados recursos de procesamiento

 

Contras

  • Dificultad para escalar
  • Costoso en recursos de almacenamiento
  • Dificultad para compartir información entre tenants

Híbrida (Semi-Isolated)

Una base de datos - Multiples schemas

Pros

  • Escalable
  • Económico
  • Sencillo
  • Seguro
  • Compartir data entre tenants

Contras

  • Dificultad para recuperación en caso de perdida de datos.
  • Menos seguro que bases de datos dedicadas (Isolated)

Implementación de multi-tenancy

con django-tenant-schemas

¿Cómo funciona?

Cada tenant (cliente) es identificado por su hostname por ejemplo (dbguiance.platzi.com).

 

La información del cliente y hostname es almacenada en una tabla en el schema public.

 

Cuando una petición es realizada, el hostname es usado para buscar el tenant en la base de datos.

 

Si no se encuentra un tenant con el hostname es lanzado un error 404

Aplicaciones especificas

La mayoría de aplicaciones dentro de un proyecto pueden ser especificas, esto significa que la información en la base de datos no se comparte con otros tenants.

 

Por ejemplo si hablamos de un software de tiendas on-line podemos tener Clientes, Proveedores, Facturas, etc...

Aplicaciones compartidas

A diferencia de las anteriores, estas son aplicaciones que pueden compartir información entre todos los tenants.

 

Seguimos hablando de la tienda on-line puede existir por ejemplo una tabla con los porcentajes de los impuestos, o tarifas de servicios de envíos, etc...

Instalación

pip install django-tenant-schemas

Configuración (básica)

En el archivo 'settings.py'

DATABASES = {
    'default': {
        'ENGINE': 'tenant_schemas.postgresql_backend',
        # ..
    }
}
DATABASE_ROUTERS = (
    'tenant_schemas.routers.TenantSyncRouter',
)
MIDDLEWARE_CLASSES = (
    'tenant_schemas.middleware.TenantMiddleware',
    # 'tenant_schemas.middleware.SuspiciousTenantMiddleware',
    #...
)

Si se desea lanzar un error 400 en lugar de un 404 cuando no se encuentra un tenant valido se usa `tenant_schemas.middleware.SuspiciousTenantMiddleware`

Es necesario asegurarse que `django.core.context_processors.request` se encuentre dentro de `TEMPLATE_CONTEXT_PROCESSORS`

El modelo Tenant

Es el modelo donde se guarda la relación entre el dominio (subdominio) y el nombre del schema, puede tener más campos pero tiene que heredar de `TenantMixin`

from django.db import models
from tenant_schemas.models import TenantMixin

class Client(TenantMixin):
    name = models.CharField(max_length=100)
    paid_until =  models.DateField()
    on_trial = models.BooleanField()
    created_on = models.DateField(auto_now_add=True)

    # default true, schema will be automatically created 
    # and synced when it is saved
    auto_create_schema = True

Una vez creado el modelo se crea el archivo de migración.

python manage.py makemigrations <nombre modelo>

Aplicaciones

Se crean dos tuplas una `SHARED_APPS` y la otra `TENANT_APPS`

 

En la primera se ponen las aplicaciones que se van a estar en el schema public y visibles por todas los tenants.

 

En la segunda se ponen las aplicaciones que tienen información separada por tenant 


SHARED_APPS = (
    'tenant_schemas',  # mandatory
    'customers', # 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 = (
    # The following Django contrib apps must be in TENANT_APPS
    'django.contrib.contenttypes',

    # your tenant-specific apps
    'myapp.hotels',
    'myapp.houses',
)

INSTALLED_APPS = list(SHARED_APPS
    ) + [app for app in TENANT_APPS if app not in SHARED_APPS]

Por último

 

Se configura el modelo tenant

 

 

Ejecutar

TENANT_MODEL = 'customer.Client"

Cuidado

Nunca usar ```./manage.py migrate```

./manage.py migrate_schemas

Configuraciones opcionales

PUBLIC_SCHEMA_NAME

Default: public

Nombre del schema que sera tratado como público

TENANT_CREATION_FAKES_MIGRATIONS

Default: True

Define si los models son migrados a directamente a la última versión y las migraciones posteriores son falseadas

PUBLIC_SCHEMA_URLCONF

Default: None

Configura las urls usadas cuando se visita el sitio principal.

 

Si consideramos que tenemos cliente.example.com y example.com la url de /login por ejemplo seria la misma en las dos urls. Definiendo este parámetro se consigue que las urls publicas sean diferentes a las compartidas por el tenant.

 

En este caso se usa las url definidas en PUBLIC_SCHEMA_URLCONF en lugar de las urls de ROOT_URLCONF

Comandos

Todos los comandos excepto `tenant_commant` corren para todos los tenants.

Para correr un comando para tenants especificos es necesario heredar de `BaseTenantCommand`

# foo/management/commands/do_foo.py

from django.core.management.base import BaseCommand

class Command(BaseCommand):
    def handle(self, *args, **options):
        do_foo()
# foo/management/commands/tenant_do_foo.py

from tenant_schemas.management.commands import BaseTenantCommand

class Command(BaseTenantCommand):
    COMMAND_NAME = 'do_foo'

Para correr el comando para un tenant en particular se usa el paramentro

--schema=<nombre schema>

tenant_command

Corre cualquier comando en un schema individual.

Si no se especifica schema, el prompt solicitara el nombre del schema, o se puede especificar así:

createsuperuser

Ya tiene configurado por defecto el parametro o flag schema

./manage.py tenant_command loaddata
./manage.py tenant_command loaddata --schema=<nombre schema>
./manage.py createsuperuser --useradmin='admin' --schema=<nombre schema>

list_tenants

Listado de schemas: domain_url de cada tenant creado

./manage.py list_tenants

Utils

schema_context

Context manager para un schema especifico

tenant_context

Context manager para un tenant especifico

schema_exists

Devuelve True si un schema ya existe en la base de datos

get_tenant_model

Devuelve el nombre de modelo tenant

get_public_schema_name

Devuelve el nombre del schema publico

Aplicaciones de terceros

Celery

Soporta Celery usando tenant-schemas-celery

 

django-debug-toolbar

Es necesario agregar los routes a urls.py manualmente.

from django.conf import settings
from django.conf.urls import include
if settings.DEBUG:
    import debug_toolbar

    urlpatterns += patterns(
        '',
        url(r'^__debug__/', include(debug_toolbar.urls)),
    )

Tests

Django no crea los tenants durante los test, para esto existe TenantTestCase.

from tenant_schemas.test.cases import TenantTestCase
from tenant_schemas.test.client import TenantClient

class BaseSetup(TenantTestCase):
    def setUp(self):
        self.c = TenantClient(self.tenant)

    def test_user_profile_view(self):
        response = self.c.get(reverse('user_profile'))
        self.assertEqual(response.status_code, 200)

Tambien existe `TenantRequestFactory` y `TenantClient` los cuales toman el dominio del tenant automagicamente.

Multi Tenants

By gollum23

Multi Tenants

  • 501