Django é uma estrutura popular que você pode selecionar para desenvolver um aplicativo para sua empresa. Mas e se você quiser criar um aplicativo SaaS que vários clientes usarão? Que arquitetura você deve escolher? Vamos ver como essa tarefa pode ser abordada.
A abordagem mais direta é criar uma instância separada para cada cliente que você possui. Digamos que temos uma aplicação Django e um banco de dados. Então, para cada cliente, precisamos executar seu próprio banco de dados e instância de aplicação. Isso significa que cada instância do aplicativo possui apenas um locatário.
Essa abordagem é simples de implementar: você precisa apenas iniciar uma nova instância de cada serviço que possui. Mas, ao mesmo tempo, pode causar um problema: cada cliente aumentará significativamente o custo da infraestrutura. Pode não ser grande coisa se você planeja ter apenas alguns clientes ou se cada instância for pequena.
Entretanto, vamos supor que estamos construindo uma grande empresa que fornece mensagens corporativas para 100.000 organizações. Imagine como pode ser caro duplicar toda a infraestrutura para cada novo cliente! E, quando precisarmos atualizar a versão do aplicativo, precisamos implantá-la para cada cliente, então a implantação também ficará mais lenta .
Existe outra abordagem que pode ajudar em um cenário em que temos muitos clientes para a aplicação: uma arquitetura multilocatário. Significa que temos vários clientes, que chamamos de inquilinos , mas todos usam apenas uma instância da aplicação.
Embora esta arquitetura resolva o problema do alto custo das instâncias dedicadas para cada cliente, ela introduz um novo problema: como podemos ter certeza de que os dados do cliente estão isolados com segurança de outros clientes?
Discutiremos as seguintes abordagens:
Usando um banco de dados compartilhado e um esquema de banco de dados compartilhado : podemos identificar qual inquilino possui os dados pela chave estrangeira que precisamos adicionar a cada tabela do banco de dados.
Usando um banco de dados compartilhado, mas esquemas de banco de dados separados : dessa forma, não precisaremos manter várias instâncias de banco de dados, mas obteremos um bom nível de isolamento de dados do locatário.
Usando bancos de dados separados : é semelhante ao exemplo de locatário único, mas não será o mesmo, pois ainda usaremos uma instância de aplicativo compartilhada e selecionaremos qual banco de dados usar verificando o locatário.
Vamos nos aprofundar nessas ideias e ver como integrá-las à aplicação Django.
Esta opção pode ser a primeira que vem à mente: adicionar uma ForeignKey às tabelas e usá-la para selecionar os dados apropriados para cada inquilino. No entanto, tem uma enorme desvantagem: os dados dos inquilinos não são isolados, portanto, um pequeno erro de programação pode ser suficiente para vazar os dados do inquilino para o cliente errado.
Vamos dar um exemplo de estrutura de banco de dados da documentação do 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)
Precisaremos identificar quais registros pertencem a qual inquilino. Então, precisamos adicionar uma tabela Tenant
e uma chave estrangeira em cada tabela 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 um pouco o código, podemos criar um modelo base abstrato que será reaproveitado em cada outro modelo que criarmos.
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 você pode ver, há pelo menos dois riscos principais aqui: um desenvolvedor pode esquecer de adicionar um campo de locatário ao novo modelo ou um desenvolvedor pode esquecer de usar esse campo ao filtrar os dados.
O código-fonte deste exemplo pode ser encontrado no GitHub: https://github.com/bp72/django-multitenancy-examples/tree/main/01_shared_database_shared_schema .
Tendo em mente os riscos do esquema compartilhado, vamos considerar outra opção: o banco de dados ainda será compartilhado, mas criaremos um esquema dedicado para cada locatário. Para implementação, podemos olhar para uma biblioteca popular Django-tenants ( documentação ).
Vamos adicionar django-tenants
ao nosso pequeno projeto (as etapas oficiais de instalação podem ser encontradas aqui ).
O primeiro passo é a instalação da biblioteca via pip
:
pip install django-tenants
Alterar os modelos: o modelo Tenant
agora estará em um aplicativo separado. Os modelos Question
e Choice
não terão mais conexão com o locatário. Como os dados de diferentes locatários estarão em esquemas separados, não precisaremos mais vincular os registros individuais às linhas do locatário.
O arquivo inquilinos/models.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 ...
O arquivo polls/models.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 Question e Choice não possuem mais uma chave estrangeira para Tenant!
A outra coisa que mudou é que o Tenant agora está em um aplicativo separado: não é apenas para separar os domínios, mas também é importante, pois precisaremos armazenar a tabela tenants
no esquema compartilhado, e tabelas polls
serão criadas para cada inquilino esquema.
Faça alterações no arquivo settings.py
para oferecer suporte a vários esquemas e locatários:
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 seguir, vamos criar e aplicar as migrações:
python manage.py makemigrations
python manage.py migrate_schemas --shared
Como resultado, veremos que o esquema público será criado e conterá apenas tabelas compartilhadas.
Precisaremos criar um locatário padrão para o esquema public
:
python manage.py create_tenant --domain-domain=default.com --schema_name=public --name=default_tenant
Defina is_primary
como True
se solicitado.
E então podemos começar a criar os verdadeiros inquilinos do serviço:
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 agora existem mais 2 esquemas no banco de dados que contêm tabelas polls
:
Agora, você obterá perguntas e opções de diferentes esquemas ao chamar APIs nos domínios configurados para os locatários. Tudo pronto!
Embora a configuração pareça mais complicada e talvez ainda mais difícil se você migrar o aplicativo existente, a abordagem em si ainda tem muitas vantagens, como a segurança dos dados.
O código do exemplo pode ser encontrado aqui .
A última abordagem que discutiremos hoje é ir ainda mais longe e ter bancos de dados separados para os inquilinos.
Desta vez, teremos alguns bancos de dados:
Armazenaremos os dados compartilhados, como o mapeamento do locatário, para os nomes dos bancos de dados no default_db
e criaremos um banco de dados separado para cada locatário.
Então precisaremos definir a configuração dos bancos de dados em settings.py:
DATABASES = { 'default': { 'NAME': 'default_db', ... }, 'tenant_1': { 'NAME': 'tenant_1', ... }, 'tenant_2': { 'NAME': 'tenant_2', ... }, }
E agora poderemos obter os dados de cada inquilino chamando using
o método QuerySet:
Questions.objects.using('tenant_1')…
A desvantagem do método é que você precisará aplicar todas as migrações em cada banco de dados usando:
python manage.py migrate --database=tenant_1
Também pode ser menos conveniente criar um novo banco de dados para cada inquilino, em comparação com o uso dos django-tenants
ou apenas usar uma chave estrangeira como na abordagem de esquema compartilhado.
Por outro lado, o isolamento dos dados do inquilino é muito bom: as bases de dados podem ser separadas fisicamente. Outra vantagem é que não ficaremos limitados em usar apenas o Postgresql conforme exigido pelos django-tenants
, podemos selecionar qualquer mecanismo que atenda às nossas necessidades.
Mais informações sobre o tópico de múltiplos bancos de dados podem ser encontradas na documentação do Django.
| Inquilino único | MT com esquema compartilhado | MT com esquema separado | MT com bancos de dados separados |
---|---|---|---|---|
Isolamento de dados | ✅Alto | ❌Mais baixo | ✅Alto | ✅Alto |
Risco de vazamento de dados acidentalmente | ✅Baixo | ❌Alto | ✅Baixo | ✅Baixo |
Custo de infraestrutura | ❌Maior com cada inquilino | ✅Inferior | ✅Inferior | ✅❌ Inferior ao de locatário único |
Velocidade de implantação | ❌Abaixar com cada inquilino | ✅ | ✅❌ As migrações serão mais lentas, pois precisam ser executadas para cada esquema | ✅❌ As migrações serão mais lentas, pois precisam ser executadas para cada banco de dados |
Fácil de implementar | ✅ | ❌ Requer muitas alterações se o serviço já tiver sido implementado como um aplicativo de locatário único | ✅ | ✅ |
Para resumir tudo o que foi dito acima, parece que não existe solução mágica para o problema, cada abordagem tem seus prós e contras, então cabe aos desenvolvedores decidir qual compensação eles podem ter.
Bancos de dados separados fornecem o melhor isolamento para os dados do locatário e são simples de implementar, no entanto, custa mais caro para manutenção: n banco de dados para atualizar, os números de conexões de banco de dados são maiores.
Um banco de dados compartilhado com um esquema separado é complexo para implementar e pode ter alguns problemas com a migração.
Inquilino único é o mais simples de implementar, mas custa-lhe pelo consumo excessivo de recursos, uma vez que tem uma cópia completa do seu serviço por inquilino.