paint-brush
Arquitetura Django SaaS: Inquilino único vs Multi-inquilino - Qual é a certa para você?por@pbityukov
2,995 leituras
2,995 leituras

Arquitetura Django SaaS: Inquilino único vs Multi-inquilino - Qual é a certa para você?

por Pavel Bityukov9m2023/11/01
Read on Terminal Reader

Muito longo; Para ler

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.
featured image - Arquitetura Django SaaS: Inquilino único vs Multi-inquilino - Qual é a certa para você?
Pavel Bityukov HackerNoon profile picture

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.

Arquitetura de locatário único

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.

A arquitetura de locatário único com instâncias dedicadas de aplicativos e bancos de dados


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 .

Arquitetura multilocatário

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.

A arquitetura multilocatário com aplicativo compartilhado e instância de banco de dados

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:

  1. 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.


  2. 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.


  3. 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.

Um banco de dados compartilhado com esquema compartilhado

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 .

Um banco de dados compartilhado com esquemas separados

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.

A estrutura do banco de dados após a primeira execução do comando Migrate_schemas

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 :

O esquema do banco de dados após a criação dos locatários

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 .

Bancos de dados separados

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:

Bancos de dados separados para os locatários


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.

Comparação


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

Conclusão

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.