見出し画像

Golangを使ったマルチテナント実装:効率的なデータ分離とパフォーマンスの向上

1つの論理的なソフトウェアアプリケーションやサービスを使用して、複数の顧客にサービスを提供する方式です。

VercelやNetlifyにアプリをデプロイしたことはありますか?もしそうであれば、アプリ用のサブドメインが自動的に生成されることにお気づきかと思います。さらに、カスタムドメインの追加やHTTPSサポートも提供されています。素晴らしいですよね?

マルチテナントアーキテクチャとは何か?

簡単に言えば、同じインフラストラクチャ上で複数の顧客(テナント)を管理する方法です。1つのアプリケーションやサービスを全てのテナントで共有し、各テナントは一意に識別されてシームレスなサービスを確保します。
複数のテナントが住む1つの家を想像してください。家がソフトウェアアプリケーションを表し、各テナントは同じ基盤を共有しながら、自分たちの専用スペースを持っています。同様に、複数のクライアントが互いに干渉することなく、同じソフトウェアを使用できます。
マルチテナントアーキテクチャの重要なポイントは、各テナントを効果的に一意に識別し、管理できることです。これが核となる概念です。
マルチテナントソフトウェアの実装方法には、以下のような種類があります:

  • カスタムドメインなし: 全てのテナントが同じドメインを共有

  • カスタムドメイン: 各テナントが独自のドメインを使用可能

カスタムドメインなしのアプローチでは、一意のテナントIDを使用してクライアントを分離します。クライアントはアカウントを作成してソフトウェアを使用しますが、ドメインはソフトウェアプロバイダーのものを使用します。これは実装が容易ですが、カスタムドメインの提供はより複雑です。

カスタムドメインのマルチテナントソフトウェアを実装する前に、Mediumのリクエストヘッダーを分析してみましょう。

Mediumもまた、カスタムドメインに対応したマルチテナントソフトウェアです。https://articles.wesionary.team/ のリクエストレスポンスを確認してみましょう。

:authority: articles.wesionary.team
:method: POST
:path: /_/graphql
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br, zstd
accept-language: en-US,en;q=0.9,ja;q=0.8
apollographql-client-name: lite
apollographql-client-version: main-20241122-011124-9eb6e2f514
cache-control: no-cache
content-length: 21346
content-type: application/json
cookie: ...
graphql-operation: CollectionViewerEdge
medium-frontend-app: lite/main-20241122-011124-9eb6e2f514
medium-frontend-path: /
medium-frontend-route: collection-homepage
origin: <https://articles.wesionary.team>
pragma: no-cache
priority: u=1, i
referer: <https://articles.wesionary.team/>
sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: same-origin
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36

ヘッダーのoriginに注目してください。他のウェブサイトのリクエストヘッダーを確認しても、必ずoriginヘッダーが含まれています。
Mediumは以下のような追加のヘッダーも送信しています:

medium-frontend-path: /
medium-frontend-route: collection-homepage

これは彼らの特定のユースケースのためかもしれませんが、シンプルな実装では他のヘッダーを送信する必要はありません。originヘッダーのみを考慮すれば十分です。

アーキテクチャ:

マルチテナント実装のアーキテクチャ

このマルチテナントアーキテクチャでは、以下の2つのドメインを処理するためにDNSをシミュレートします:

  • api.multitenant.com

  • multitenant.com

これらのドメインは私たちが所有していないため、DNSの解決をシミュレートするために/etc/hostsファイルを使用します。/etc/hostsファイルに以下のエントリを追加してください:

127.0.0.1 api.multitenant.com
127.0.0.1 multitenant.com

この設定により、api.multitenant.comまたはmultitenant.comにpingを実行すると、それらのIPアドレスは127.0.0.1(localhost)に解決されます。
アーキテクチャ図から説明すると:

  • HTMLコンテンツはポート8000で実行されているPythonサービス(または他の選択したサービス)によって提供されます。

  • バックエンドサービスはポート8080で実行されています。[以下の実装を参照]

http://multitenant.comへのリクエストはポート8000で実行されているサービスに転送され、同様にhttp://api.multitenant.comへのリクエストはポート8080で実行されているバックエンドサービスに転送されます。
この動作を実現するために、リバースプロキシを使用します。この設定では、Caddyをリバースプロキシとして使用しています。以下がトラフィックを正しくルーティングするためのCaddyの設定です:

<http://api.multitenant.com> {
    reverse_proxy localhost:8080
}

<http://multitenant.com> {
    reverse_proxy localhost:8000
}

この設定により:

  1. http://multitenant.comへのリクエストはポート8000のHTML提供サービスにルーティングされます。

  2. http://api.multitenant.comへのリクエストはポート8080のバックエンドサービスにルーティングされます。

この設定により、マルチテナントアーキテクチャの適切なルーティングが確保され、フロントエンドとバックエンドサービスの分離が容易になります。

同一サービスを指す2つのドメイン

では、Golangでのマルチテナントアーキテクチャの主要な実装について見ていきましょう。

Golangを使用したマルチテナントアーキテクチャの実装

共有データベースとテーブルを使用してマルチテナントアーキテクチャを実装します。各テナントのデータは、一意のtenant_idを使用して分離されます。
マルチテナントドメイン

|         domain        | tenant_id |
|-----------------------|-----------|
|localhost:8000         | 12345     |
|multitenant.com        | 11223     |

...

テナント情報

| tenant_id |      detail           |
|-----------|-----------------------|
| 12345     | I am localhost        |
| 11223     | I am multitenant      |
...

マルチテナント実装の手順

Originヘッダーの抽出

  • フロントエンドとバックエンドが同じドメインを共有している場合、バックエンドでHostヘッダーを使用できます。

  • しかし、Vue、React、Svelteなどの最新のシングルページアプリケーション(SPA)は、異なるドメインにデプロイされることが多いため、APIはクライアントの実際のドメインを識別するためにOriginヘッダーを使用してバックエンドと通信します。

永続的ストレージを使用したOriginからのテナントIDの取得

  • ミドルウェアは、永続的データベースに保存されているドメイン(Originヘッダーから抽出)に基づいてtenant_idを取得します。

テナントIDを使用したデータの分離

  • すべての操作はtenant_idを使用して、各テナントのデータ分離を確実に行います。

コード実装

テナントID抽出用ミドルウェア

func (t *TenantMiddleware) ExtractTenantIDFromDomain() gin.HandlerFunc {
 return func(c *gin.Context) {
  // Extract the header origin
  origin := c.Request.Header.Get("Origin")
  host := c.Request.Host
  domain := origin
  if domain == "" {
   domain = host
  } else if strings.Contains(domain, constants.HTTP) {
   domain = strings.Replace(domain, constants.HTTP, "", -1)
  } else if strings.Contains(domain, constants.HTTPS) {
   domain = strings.Replace(domain, constants.HTTPS, "", -1)
  }
  
  // Fetch the tenant id from the origin using persistent storage
  tenantID := t.db.FindTenantIDByDomain(domain)
  c.Set(constants.TenantID, tenantID)
  c.Next()
 }
}

テナント詳細取得用ハンドラー

func (h *Handler) GetDetail(c *gin.Context) {
 tenantID, exists := c.Get(constants.TenantID)
 if !exists {
  c.AbortWithStatusJSON(http.StatusBadRequest, &Error{
   Message: "tenant not found",
  })
 }
 // Use the tenant id in order to isolate the data from other tenant
 detail := h.db.FindDetailByTenantID(tenantID.(string))

 c.JSON(http.StatusOK, &Response{
  Data: detail,
 })
}

完全なコードは以下のリポジトリをご確認ください。
GitHubリポジトリ - mukezhz/simple-multitenant

アプリケーションの実行手順:

以下のドメインを/etc/hostsに追加してください

/etc/hostsを以下のエントリで更新:

マルチテナント環境をローカルでシミュレートするために、カスタムドメインを127.0.0.1にマッピングするよう/etc/hostsファイルを更新します。以下の行を追加してください:

...
127.0.0.1 api.multitenant.com
127.0.0.1 multitenant.com

これにより、カスタムドメインがローカルマシンで正しく解決されます。

Ginアプリケーションの実行:
Golangで書かれたバックエンドサービスを実行:

go run cmd/main.go

これによりアプリケーションのAPIリクエストを処理するサーバーが起動します。

Caddyサーバーの実行:
アプリケーションのリバースプロキシを管理するためのCaddyサーバーを起動:

caddy run --config Caddyfile

Caddyはウェブサーバーとして機能し、カスタムドメインのルーティングを処理します。

フロントエンドの提供:
フロントエンドファイルをローカルで提供するための軽量HTTPサーバーを起動:

python -m http.server 8000

これによりウェブブラウザからフロントエンドアプリケーションにアクセスできるようになります。

アプリケーションのテスト:

  • http://localhost:8000またはhttp://multitenant.comにアクセスしてください。

  • ネットワークタブでリクエストヘッダーを確認してください。

出力例

http://multitenant.comにアクセスした場合:

{"data":"I am multitenant"}
multitenant.comのネットワークタブ

http://localhost:8000にアクセスした場合:

{"data":"I am localhost"}
localhost:8000のネットワークタブ

重要なポイント

ドメインを使用してテナントを識別する基本的なマルチテナントアプリケーションを実装しました。バックエンドはすべてのテナントに対して共有インフラストラクチャ(同じデータベース)を使用しますが、tenant_idを使用してデータを分離します。
テナントがより高い要件(大量のデータやトラフィックなど)を持つ場合、そのテナント専用のデータベースなど、個別のインフラストラクチャを使用する必要があるかもしれません。
次の記事では、テナントが追加したカスタムドメインに対してHTTPS証明書を自動生成する方法について説明します。お楽しみに!

参考文献:

マルチテナントアーキテクチャ:仕組み、メリット、デメリット | Frontegg
マルチテナントアーキテクチャの探求:包括的ガイド
マルチテナントアプリケーション
マルチテナントアーキテクチャの完全ガイド

ありがとうございました!


この記事は、2024 年 12 月に弊社のエンジニア Mukesh Chaudhary が執筆し、日本語に翻訳したものです。
英語版はこちらをクリックしてください。
https://articles.wesionary.team/building-a-multi-tenant-architecture-in-golang-a-practical-guide-8ee066436678


採用情報

私たちはプロダクト共創の仕組み化に取り組んでいます。プロダクト共創をリードするプロダクト・マネージャー、そして、私たちのビジョンを市場に届ける営業メンバーを募集しています!


開発パートナーをお探しの企業様へ

弊社は、グローバル開発のメリットを活かし、高い費用対効果と品質を両立しています。経験豊富で多様性のあるチームが、課題を正しく理解し、最適なシステムと優れた体験を実現します。業務システムの開発、新規事業の開発、業務効率化やDX化に関するお困りごと、ぜひ弊社にご相談ください。