Django は、会社のアプリケーションを開発するために選択できる人気のあるフレームワークです。しかし、複数のクライアントが使用する SaaS アプリケーションを作成したい場合はどうすればよいでしょうか?どのようなアーキテクチャを選択すればよいでしょうか?このタスクにどのようにアプローチできるかを見てみましょう。
最も簡単なアプローチは、クライアントごとに個別のインスタンスを作成することです。 Django アプリケーションとデータベースがあるとします。次に、クライアントごとに独自のデータベースとアプリケーション インスタンスを実行する必要があります。つまり、各アプリケーション インスタンスにはテナントが 1 つだけ存在します。
このアプローチは実装が簡単です。必要なのは、所有しているすべてのサービスの新しいインスタンスを開始するだけです。しかし同時に、問題が発生する可能性があります。各クライアントのインフラストラクチャのコストが大幅に増加することになります。クライアントの数がわずかである場合、または各インスタンスが小さい場合は、大した問題ではないかもしれません。
ただし、100,000 の組織に企業メッセンジャーを提供する大企業を構築していると仮定しましょう。新しいクライアントごとにインフラストラクチャ全体を複製するのにどれだけの費用がかかるかを想像してみてください。また、アプリケーションのバージョンを更新する必要がある場合、クライアントごとに展開する必要があるため、展開も遅くなります。
アプリケーションに多数のクライアントがある場合に役立つもう 1 つのアプローチ、それがマルチテナント アーキテクチャです。これは、テナントと呼ばれる複数のクライアントが存在しますが、それらはすべてアプリケーションの 1 つのインスタンスのみを使用することを意味します。
このアーキテクチャは、各クライアントの専用インスタンスの高コストの問題を解決しますが、クライアントのデータが他のクライアントから安全に分離されていることをどのように確認できるかという新しい問題を引き起こします。
次のアプローチについて説明します。
共有データベースと共有データベース スキーマの使用: 各データベース テーブルに追加する必要がある外部キーによって、どのテナントがデータを所有しているかを識別できます。
共有データベースを使用するが、別個のデータベース スキーマを使用する: この方法では、複数のデータベース インスタンスを維持する必要がなく、適切なレベルのテナント データ分離が得られます。
個別のデータベースの使用 : シングルテナントの例に似ていますが、同じではありません。共有アプリケーション インスタンスを引き続き使用し、テナントを確認して使用するデータベースを選択します。
これらのアイデアをさらに深く掘り下げて、それらを Django アプリケーションと統合する方法を見てみましょう。
このオプションは、テーブルにForeignKeyを追加し、それを使用して各テナントに適切なデータを選択するために最初に思い浮かぶかもしれません。ただし、これには大きな欠点があります。テナントのデータはまったく分離されていないため、小さなプログラミング エラーでテナントのデータが間違ったクライアントに漏洩する可能性があります。
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)
どのレコードがどのテナントによって所有されているかを特定する必要があります。したがって、既存の各テーブルにTenant
テーブルと外部キーを追加する必要があります。
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)
コードを少し単純化するために、作成する他の各モデルで再利用される抽象的な基本モデルを作成できます。
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)
ご覧のとおり、ここには少なくとも 2 つの大きなリスクがあります。開発者が新しいモデルにテナント フィールドを追加することを忘れる可能性があること、または開発者がデータをフィルタリングするときにこのフィールドを使用することを忘れる可能性があることです。
この例のソース コードは、GitHub: https://github.com/bp72/django-multitenancy-examples/tree/main/01_shared_database_shared_schemaにあります。
共有スキーマのリスクを念頭に置いて、別のオプションを検討してみましょう。データベースは引き続き共有されますが、テナントごとに専用のスキーマを作成します。実装については、人気のあるライブラリdjango-tenants (ドキュメント) を参照してください。
私たちの小さなプロジェクトにdjango-tenants
を追加しましょう (公式のインストール手順はここにあります)。
最初のステップは、 pip
によるライブラリのインストールです。
pip install django-tenants
モデルを変更します。 Tenant
モデルは別のアプリになり、 Question
モデルとChoice
モデルはテナントとの接続を失います。異なるテナントのデータは個別のスキーマに存在するため、個々のレコードをテナントの行にリンクする必要はなくなります。
ファイルtenants/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 ...
ファイル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)
Question と Choice には Tenant への外部キーが存在しないことに注意してください。
もう 1 つの変更点は、テナントが別のアプリ内にあることです。これはドメインを分離するためだけでなく、共有スキーマにtenants
テーブルを保存する必要があるため、テナントごとにpolls
テーブルが作成されるため重要です。スキーマ。
複数のスキーマとテナントをサポートするためにsettings.py
ファイルを変更します。
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"
次に、移行を作成して適用しましょう。
python manage.py makemigrations
python manage.py migrate_schemas --shared
その結果、パブリック スキーマが作成され、共有テーブルのみが含まれることがわかります。
public
スキーマのデフォルト テナントを作成する必要があります。
python manage.py create_tenant --domain-domain=default.com --schema_name=public --name=default_tenant
要求されたら、 is_primary
をTrue
に設定します。
次に、サービスの実際のテナントの作成を開始できます。
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
データベースには、 polls
テーブルを含むスキーマがさらに 2 つあることに注意してください。
これで、テナント用に設定したドメインで API を呼び出すときに、さまざまなスキーマから質問と選択肢を取得できるようになり、これですべて完了です。
セットアップはより複雑に見え、既存のアプリを移行する場合はさらに困難になる可能性がありますが、このアプローチ自体にはデータのセキュリティなど多くの利点があります。
サンプルのコードはここにあります。
今日説明する最後のアプローチは、さらに進んで、テナント用に個別のデータベースを用意することです。
今回は、いくつかのデータベースを用意します。
テナントのデータベース名へのマッピングなどの共有データをdefault_db
に保存し、テナントごとに個別のデータベースを作成します。
次に、settings.py でデータベース構成を設定する必要があります。
DATABASES = { 'default': { 'NAME': 'default_db', ... }, 'tenant_1': { 'NAME': 'tenant_1', ... }, 'tenant_2': { 'NAME': 'tenant_2', ... }, }
これで、QuerySet メソッドusing
呼び出して、各テナントのデータを取得できるようになります。
Questions.objects.using('tenant_1')…
この方法の欠点は、以下を使用して各データベースにすべての移行を適用する必要があることです。
python manage.py migrate --database=tenant_1
また、テナントごとに新しいデータベースを作成することは、 django-tenants
使用する場合や、共有スキーマ アプローチのように外部キーを単に使用する場合に比べて、利便性が劣る可能性があります。
一方、テナントのデータの分離は非常に優れており、データベースを物理的に分離できます。もう 1 つの利点は、 django-tenants
で必要とされる Postgresql のみの使用に制限されず、ニーズに合った任意のエンジンを選択できることです。
複数データベースのトピックの詳細については、Django のドキュメントを参照してください。
| シングルテナント | 共有スキーマを使用した MT | 別のスキーマを使用した MT | 別個のデータベースを使用した MT |
---|---|---|---|---|
データの分離 | ✅高い | ❌最低 | ✅高い | ✅高い |
誤ってデータが漏洩する危険性 | ✅低い | ❌高い | ✅低い | ✅低い |
インフラコスト | ❌テナントごとに高くなる | ✅下段 | ✅下段 | ✅❌ シングルテナントよりも低い |
導入速度 | ❌テナントごとに値下げ | ✅ | ✅❌ 移行はスキーマごとに実行する必要があるため、時間がかかります。 | ✅❌ 移行はデータベースごとに実行する必要があるため、時間がかかります。 |
実装が簡単 | ✅ | ❌ サービスがすでにシングルテナント アプリとして実装されている場合は、多くの変更が必要です | ✅ | ✅ |
上記をすべて要約すると、この問題に対する特効薬はないようです。それぞれのアプローチには長所と短所があるため、どのようなトレードオフがあるかを決定するのは開発者次第です。
個別のデータベースはテナントのデータを最適に分離し、実装が簡単ですが、メンテナンスのコストが高くなります。更新するデータベースが多くなり、データベースの接続数も多くなります。
別のスキーマを持つ共有データベースの実装は少し複雑で、移行時に問題が発生する可能性があります。
シングル テナントは実装が最も簡単ですが、テナントごとにサービスのコピー全体が存在するため、リソースの過剰消費によるコストがかかります。