Implementação de multi-tenant com PostgreSQL: Aprenda por meio de um exemplo simples do mundo real
Aprenda como implementar uma arquitetura multi-tenant com a Segurança em Nível de Linha (RLS) do PostgreSQL e funções de banco de dados por meio de um exemplo do mundo real para isolamento seguro de dados entre locatários.
Em alguns de nossos artigos anteriores, exploramos o conceito de multi-tenancy e suas aplicações em produtos e cenários de negócios do mundo real.
Neste artigo, exploraremos como implementar uma arquitetura multi-tenant para sua aplicação usando o PostgreSQL de uma perspectiva técnica.
O que é arquitetura single-tenant?
Arquitetura single-tenant refere-se a uma arquitetura de software onde cada cliente possui sua própria instância dedicada da aplicação e banco de dados.
Nesta arquitetura, os dados e recursos de cada locatário são completamente isolados de outros locatários.
O que é arquitetura multi-tenant?
Arquitetura multi-tenant é uma arquitetura de software onde múltiplos clientes (locatários) compartilham a mesma instância de aplicação e infraestrutura, mantendo ao mesmo tempo o isolamento dos dados. Nesta arquitetura, uma única instância do software atende a múltiplos locatários, com os dados de cada locatário mantidos separados dos outros através de vários mecanismos de isolamento.
Arquitetura single-tenant vs arquitetura multi-tenant
Arquitetura single-tenant e arquitetura multi-tenant diferem em aspectos como isolamento de dados, utilização de recursos, escalabilidade, gestão e manutenção, e segurança.
Na arquitetura single-tenant, cada cliente possui um espaço de dados independente, levando a uma menor utilização de recursos, mas relativamente mais simples para personalização. Tipicamente, software single-tenant é adaptado às necessidades específicas dos clientes, como sistemas de inventário para um fornecedor de tecido específico ou um aplicativo web de blog pessoal. A característica comum entre eles é que cada cliente ocupa uma instância separada do serviço de aplicação, facilitando a personalização para atender a requisitos específicos.
Em uma arquitetura multi-tenant, múltiplos locatários compartilham os mesmos recursos subjacentes, resultando em maior utilização de recursos. No entanto, é crucial assegurar o isolamento de dados e segurança.
Arquitetura multi-tenant é muitas vezes a arquitetura de software preferida quando provedores de serviço oferecem serviços padronizados para diferentes clientes. Esses serviços tipicamente têm baixos níveis de personalização, e todos os clientes compartilham a mesma instância de aplicação. Quando uma aplicação requer uma atualização, atualizar uma instância de aplicação é equivalente a atualizar a aplicação para todos os clientes. Por exemplo, CRM (Customer Relationship Management) é um requisito padronizado. Esses sistemas tipicamente usam uma arquitetura multi-tenant para fornecer o mesmo serviço para todos os locatários.
Estratégias de isolamento de dados de locatários em uma arquitetura multi-tenant
Em uma arquitetura multi-tenant, todos os locatários compartilham os mesmos recursos subjacentes, tornando crucial o isolamento de recursos entre os locatários. Este isolamento não precisa necessariamente ser físico; simplesmente requer assegurar que os recursos entre os locatários não sejam visíveis uns aos outros.
No design da arquitetura, vários graus de isolamento de recursos entre locatários podem ser alcançados:
Em geral, quanto mais recursos compartilhados entre locatários, menor o custo de iteração e manutenção do sistema. Por outro lado, quanto menos recursos compartilhados, maior o custo.
Começando a implementação multi-tenant com um exemplo do mundo real
Neste artigo, usaremos um sistema CRM como exemplo para introduzir uma arquitetura multi-tenant simples, mas prática.
Reconhecemos que todos os locatários usam os mesmos serviços padrão, então decidimos que todos os locatários compartilharão os mesmos recursos básicos, e implementaremos o isolamento de dados entre diferentes locatários no nível do banco de dados usando a Segurança em Nível de Linha do PostgreSQL.
Além disso, criaremos uma conexão de dados separada para cada locatário para facilitar uma melhor gestão das permissões dos locatários.
Em seguida, apresentaremos como implementar esta arquitetura multi-tenant.
Como implementar a arquitetura multi-tenant com PostgreSQL
Adicionar identificador de locatário para todos os recursos
Em um sistema CRM, teremos muitos recursos e eles estão armazenados em diferentes tabelas. Por exemplo, informações do cliente são armazenadas na tabela customers
.
Antes de implementar multi-tenancy, esses recursos não estão associados a nenhum locatário:
Para diferenciar os locatários que possuem diferentes recursos, introduzimos uma tabela tenants
para armazenar informações dos locatários (onde db_user
e db_user_password
são usados para armazenar as informações de conexão do banco de dados para cada locatário, será detalhado abaixo). Além disso, adicionamos um campo tenant_id
a cada recurso para identificar a qual locatário ele pertence:
Agora, cada recurso está associado a um tenant_id
, teoricamente nos permitindo adicionar uma cláusula where
a todas as consultas para restringir o acesso aos recursos de cada locatário:
À primeira vista, isso parece simples e viável. No entanto, terão os seguintes problemas:
- Quase todas as consultas incluirão esta cláusula
where
, poluindo o código e tornando-o mais difícil de manter, especialmente ao escrever declarações complexas de junção. - Novos desenvolvedores no código-base podem facilmente esquecer de adicionar esta cláusula
where
. - Dados entre diferentes locatários não estão verdadeiramente isolados, pois cada locatário ainda tem permissões para acessar dados pertencentes a outros locatários.
Portanto, não adotaremos essa abordagem. Em vez disso, usaremos a Segurança em Nível de Linha do PostgreSQL para abordar essas preocupações. No entanto, antes de prosseguir, criaremos uma conta de banco de dados dedicada para cada locatário acessar este banco de dados compartilhado.
Configurar funções de BD para locatários
É uma boa prática atribuir uma função de banco de dados para cada usuário que pode se conectar ao banco de dados. Isso permite um melhor controle sobre o acesso de cada usuário ao banco de dados, facilitando o isolamento de operações entre diferentes usuários e melhorando a estabilidade e segurança do sistema.
Como todos os locatários têm as mesmas permissões de operação de banco de dados, podemos criar uma função base para gerenciar essas permissões:
Depois, para diferenciar cada função de locatário, uma função herdada da função base é atribuída a cada locatário na criação:
Em seguida, as informações de conexão do banco de dados para cada locatário serão armazenadas na tabela tenants
:
id | db_user | db_user_password |
---|---|---|
x2euic | crm_tenant_x2euic | pa55w0rd |
Este mecanismo fornece a cada locatário sua própria função de banco de dados, e essas funções compartilham as permissões concedidas à função crm_tenant
.
Podemos então definir o escopo de permissão para os locatários usando a função crm_tenant
:
- Os locatários devem ter acesso CRUD a todas as tabelas de recursos do sistema CRM.
- Tabelas não relacionadas aos recursos do sistema CRM devem ser invisíveis aos locatários (assumindo apenas a tabela
systems
). - Locatários não devem poder modificar a tabela
tenants
, e somente os camposid
edb_user
devem ser visíveis para eles ao consultar seu próprio id de locatário ao realizar operações de banco de dados.
Uma vez que as funções para os locatários estão configuradas, quando um locatário solicita acesso ao serviço, podemos interagir com o banco de dados usando a função de banco de dados que representa esse locatário:
Proteger dados dos locatários usando Segurança em Nível de Linha do PostgreSQL
Até agora, estabelecemos funções de banco de dados correspondentes para locatários, mas isso não restringe o acesso aos dados entre locatários. Em seguida, vamos aproveitar o recurso de Segurança em Nível de Linha do PostgreSQL para limitar o acesso de cada locatário aos seus próprios dados.
No PostgreSQL, tabelas podem ter políticas de segurança em nível de linha que controlam quais linhas podem ser acessadas por consultas ou modificadas por comandos de manipulação de dados. Este recurso também é conhecido como RLS (Segurança em Nível de Linha).
Por padrão, tabelas não têm políticas de segurança em nível de linha. Para utilizar o RLS, você precisa habilitá-lo para a tabela e criar políticas de segurança que são executadas toda vez que a tabela é acessada.
Tomando como exemplo a tabela customers
no sistema CRM, habilitaremos o RLS e criaremos uma política de segurança para restringir que cada locatário apenas tenha acesso aos dados de seus próprios clientes:
Na declaração criando a política de segurança:
for all
(opcional) indica que esta política de acesso será usada para operações deselect
,insert
,update
, edelete
na tabela. Você pode especificar uma política de acesso para operações específicas usandofor
seguido pelo comando.to crm_tenant
indica que esta política se aplica a usuários com a função de banco de dadoscrm_tenant
, significando todos os locatários.as restrictive
especifica o modo de aplicação da política, indicando que o acesso deve ser estritamente limitado. Por padrão, uma tabela pode ter várias políticas, várias políticaspermissive
serão combinadas com uma relaçãoOR
. Neste cenário, declaramos esta política comorestrictive
porque queremos que esta verificação de política seja obrigatória para usuários pertencentes aos locatários do sistema CRM.using
expressão define as condições para o acesso real, restringindo o usuário atual do banco de dados ao visualizar apenas dados pertencentes a seu respectivo locatário.with check
expressão define a restrição necessária ao modificar linhas de dados (insert
ouupdate
), garantindo que os locatários só possam adicionar ou atualizar registros para si mesmos.
Usar RLS para limitar o acesso dos locatários às nossas tabelas de recurso oferece diversos benefícios:
- Esta política efetivamente adiciona
where tenant_id = (select id from tenants where db_user = current_user)
a todas as operações de consulta (select
,update
, oudelete
). Por exemplo, quando você executaselect * from customers
, é equivalente a executarselect * from customers where tenant_id = (select id from tenants where db_user = current_user)
. Isso elimina a necessidade de adicionar explicitamente condições dewhere
no código da aplicação, simplificando-o e reduzindo a probabilidade de erros. - Ela controla centralmente permissões de acesso de dados entre diferentes locatários no nível do banco de dados, mitigando o risco de vulnerabilidades ou inconsistências na aplicação, assim melhorando a segurança do sistema.
No entanto, há alguns pontos a notar:
- Políticas de RLS são executadas para cada linha de dados. Se as condições de consulta dentro da política RLS forem muito complexas, isso pode impactar significativamente o desempenho do sistema. Felizmente, nossa consulta de verificação de dados de locatário é simples o suficiente e não afetará o desempenho. Se você planeja implementar outras funcionalidades usando RLS mais tarde, pode seguir as recomendações de desempenho de Segurança em Nível de Linha do Supabase para otimizar o desempenho do RLS.
- Políticas de RLS não preenchem automaticamente
tenant_id
durante operações deinsert
. Elas apenas restringem os locatários a inserir seus próprios dados. Isso significa que ao inserir dados, ainda precisamos fornecer o ID do locatário, o que é inconsistente com o processo de consulta e pode levar a confusões durante o desenvolvimento, aumentando a probabilidade de erros (isso será abordado em etapas subsequentes).
Além da tabela customers
, precisamos aplicar as mesmas operações a todas as tabelas de recursos do sistema CRM (este processo pode ser um pouco tedioso, mas podemos escrever um programa para configurá-lo durante a inicialização das tabelas), assim isolando dados de diferentes locatários.
Criar função de gatilho para inserção de dados
Como mencionado anteriormente, RLS (Segurança em Nível de Linha) nos permite executar consultas sem nos preocupar com a existência de tenant_id
, já que o banco de dados o gerencia automaticamente. No entanto, para operações de insert
, ainda precisamos especificar manualmente o tenant_id
correspondente.
Para conseguir uma conveniência semelhante ao RLS para inserção de dados, precisamos que o banco de dados seja capaz de lidar automaticamente com o tenant_id
durante a inserção de dados.
Isto tem um claro benefício: no nível de desenvolvimento da aplicação, não precisamos mais considerar a qual locatário os dados pertencem, reduzindo a probabilidade de erros e aliviando a carga mental ao desenvolver aplicativos multi-tenant.
Felizmente, o PostgreSQL fornece uma poderosa funcionalidade de gatilho.
Gatilhos são funções especiais associadas a tabelas que automaticamente executam ações específicas (como inserir, atualizar ou excluir) quando realizadas na tabela. Estas ações podem ser acionadas no nível de linha (para cada linha) ou no nível de declaração (para toda a declaração). Com gatilhos, podemos executar lógica personalizada antes ou depois de operações específicas de banco de dados, permitindo-nos facilmente atingir nosso objetivo.
Primeiro, vamos criar uma função de gatilho set_tenant_id
para ser executada antes de cada inserção de dados:
Em seguida, associar esta função de gatilho à tabela customers
para operações de inserção (semelhante a habilitar RLS para uma tabela, esta função de gatilho precisa ser associada a todas as tabelas relevantes):
Este gatilho assegura que os dados inseridos contenham o tenant_id
correto. Se os novos dados já incluem um tenant_id
, a função de gatilho não faz nada. Caso contrário, ele preenche automaticamente o campo tenant_id
com base nas informações do usuário atual na tabela tenants
.
Desta forma, conseguimos lidar automaticamente com o tenant_id
no nível do banco de dados durante a inserção de dados pelos locatários.
Resumo
Neste artigo, exploramos a aplicação prática de uma arquitetura multi-tenant, usando um sistema CRM como exemplo para demonstrar uma solução prática utilizando banco de dados PostgreSQL.
Discutimos a gestão de funções de banco de dados, controle de acesso e o recurso de Segurança em Nível de Linha do PostgreSQL para assegurar o isolamento de dados entre locatários. Além disso, utilizamos funções de gatilho para reduzir a carga cognitiva dos desenvolvedores na gestão de diferentes locatários.
É isso para este artigo. Se você deseja aprimorar ainda mais sua aplicação multi-tenant com gestão de acesso de usuários, pode se referir a Um guia fácil para começar com organizações Logto - para construção de um app multi-tenant para mais insights.