SlideShare une entreprise Scribd logo
1  sur  36
Télécharger pour lire hors ligne
© 2022 LayerX Inc.
1
マルチテナントのアプリケーション実装
〜実践編〜
SaaSにおけるマルチテナント設計の悩みと勘所
SaaS.tech #2
2022.04.20
© 2022 LayerX Inc.
2
自己紹介
株式会社LayerX
中川佳希
@yyoshiki41
バクラク請求書のテックリード
バックエンドからフロントエンドまで.
SaaS が扱う業務ドメインへの好奇心と
サービスの成長に日々ワクワクを感じています.
Gopher.
© 2022 LayerX Inc.
3
1. 導入
a. マルチテナントSaaS
2. データベース設計
a. 設計パターン
3. アプリケーション側での実装
a. テーブルスキーマ
b. 型(Type)の実装
c. コンテキスト(Request-scoped Data)
d. ORM
e. バリデーション
f. ロギング / モニタリング
g. テスト
章立て
© 2022 LayerX Inc.
4
マルチテナント SaaS
マルチテナント SaaS は・・・
同質のソフトウェアを全テナントのユーザーへ提供.
リソースの一部またはすべてをテナント間で共有.
SaaS 提供側のメリット(シングルテナントアプリケーションと比較)
● 機能提供アウトカムの最大化
● サービス運用の効率化
● インフラコストの削減
© 2022 LayerX Inc.
5
開発者を悩ますポイント
1. テナント毎に安全にデータを分離した状態で、アプリケーションを実装できるか?
2. テナント境界線をどのレイヤで実装するか?
3. テナント間のシステムリソース共有をどこまで行うか?
マルチテナント SaaS が絶対に防ぐべきこと
=> データが他のテナントにも共有されてしまうこと(漏洩)
マルチテナント SaaS
© 2022 LayerX Inc.
6
マルチテナント SaaS での安全なデータ分離の実装
安全にテナント毎のデータを分離するには?
● データベース
● ミドルウェア
○ アプリ => ミドルウェア => データベース のような Proxy を想定
● アプリケーション
● テスト
以降では低レイヤの部分から順にみていきます.
© 2022 LayerX Inc.
7
SaaS 開発における最初の分岐点
1. データベースをテナント毎に作成
=> データ所有者(テナント)を データベース名 で表現
2. データベースは共有、スキーマをテナント毎に作成
=> データ所有者(テナント)を スキーマ で表現
3. データベースは共有、テーブルをテナント毎に作成
=> データ所有者(テナント)を テーブル名 で表現
4. データベースは共有、テーブルも共有、
各テーブルがテナント識別カラムを持ち、レコード値で識別・分離
=> データ所有者(テナント)を テーブルのカラム値 で表現
データベース設計
Isolated
Shared
© 2022 LayerX Inc.
8
1. データベースをテナント毎に作成
メリット
1. データの持ち方としては、もっとも堅牢
デメリット
1. マイグレーション(テーブルスキーマ変更)コスト
2. アプリからのデータベース接続コスト
a. データベースユーザーが異なる場合, セッションも異なる
3. テナント作成時に、毎回データベースの初期化処理が必要
a. テーブル作成、データベースユーザーの認証設定
4. テナントとデータベースのリレーションが別途必要
Tenant B
Tenant A
© 2022 LayerX Inc.
9
スキーマとは...
PostgreSQL などで名前空間を作成できる仕組み.
データベースオブジェクト(テーブル、関数など)を
同じオブジェクト名でもスキーマが異なれば作成可能になる.
MySQL ではオブジェクトを論理的に分ける
(グルーピングする)仕組みに相当するものは見当たらず.
2. データベースは共有、スキーマをテナント毎に作成
Global
Tables
Tenant A
Tables
Tenant B
© 2022 LayerX Inc.
10
2. データベースは共有、スキーマをテナント毎に作成
Global
Tables
Tenant A
Tables
Tenant B
メリット
1. スキーマ単位で所有者を決めれる
デメリット
1. マイグレーション(テーブルスキーマ変更)コスト
2. アプリからのデータベース接続コスト
a. スキーマ所有者が異なる場合, セッションも異なる
3. テナント作成時に、毎回スキーマの初期化処理が必要
a. テーブル作成、スキーマ所有者の認証設定
4. テナントとスキーマのリレーションが別途必要
© 2022 LayerX Inc.
11
メリット
1. 1つのデータベース内でテナントのデータを保持できる
デメリット
1. マイグレーション(テーブルスキーマ変更)コスト
2. テナント作成時に、毎回テーブルの初期化処理が必要
a. テーブル作成
3. テナントとテーブル名のリレーションが別途必要
4. テナントのテーブル間で外部キーが煩雑になる
3. データベースは共有、テーブルをテナント毎に作成
Global
Tenant A
tables
Tenant B
tables
© 2022 LayerX Inc.
12
ID TenantID Name
1 A Foo
2 B Bar
メリット
1. マイグレーション(テーブルスキーマ変更)コストが低い
2. データベース側での設定コストが低い
デメリット
1. 他テナントのレコードへアクセスが容易
a. 同一テーブルの為, 最もカジュアルにアクセス可能
2. ロジックを実装する必要がある
a. アプリ側で制御する場合
i. WHERE 句
b. データベース側で制御する場合
i. Row-Level Security での制御
ii. ストアドプロシージャを実装しての制御
4. テーブルにテナント識別カラムを持ち, 行単位で制御
Global
tables
© 2022 LayerX Inc.
13
テーブルに識別カラムを持ち,
テーブルへのポリシー × データベースユーザーで制御するをデータベース側で実装す
る例
データベースユーザーのセッション管理などが一定ネックになる.
● PostgreSQL Row Level Security
○ PostgreSQL ネイティブの機能
● Implementing row level security in MySQL / SQL Maestro
○ Trigger や View テーブルを使って, MySQL で RLS を実現する例
4. テーブルにテナント識別カラムを持ち, 行単位で制御
© 2022 LayerX Inc.
14
アプリ => ミドルウェア => データベース のようなプロキシを想定.
● ProxySQL
○ mysql_query_rules: WHERE 句に TenantID がないクエリをはじく
● MariaDB MaxScale
○ Deny ルールを作る: WHERE 句に TenantID がないクエリをはじく
上記のような SQL を解釈できるプロキシミドルウェアで,
ポリシー(とデータベースユーザーの掛け合わせ)での制御.
ポリシーに沿わないレコードへのアクセスを拒否する.
実検証までは行っておらず 🙏
ミドルウェア(+α)
© 2022 LayerX Inc.
15
4. テーブルにテナント識別カラムを持ち, 行単位で制御
主な理由
● データベース、マイグレーション運用コストが最も低い
○ デプロイ(マイグレーション)難易度が高いサービスは致命的
○ テナント追加時, データベース側の初期化処理のサブシステム等が不要
● データベース側にロジックを寄せることでのロックインを避けたい
○ ストアドプロシージャなどの実装への依存も持ちたくない
● テナント数のスケールに最も適している
○ テナント毎のデータベースユーザーと管理, アプリからの接続セッションの管
理なども不要
● ロジックのテストの行いやすさ
バクラクでのデータベース設計
© 2022 LayerX Inc.
16
4. テーブルにテナント識別カラムを持ち, 行単位で制御
テナント毎にデータを(論理的に)識別・分離して扱うことは, 至上命題.
実際の取り組み(ここからが本題)
1. テーブルスキーマ
2. 型(Type)の実装
3. コンテキスト(Request-scoped Data)
4. ORM
5. バリデーション
6. ロギング / モニタリング
7. テスト
アプリケーション実装〜実践編〜
ID TenantID Name
1 A Foo
2 B Bar
© 2022 LayerX Inc.
17
全テーブルへ冗長に TenantID (テナント識別カラム)を付ける
(中間テーブル、子テーブルでも同様)
親テーブルにはマストで必要.
子テーブルでは従来, 親テーブルへの外部キー制約だけで充分.
冗長にカラムを付ける理由は,
1. ORM などで, WHERE句に TenantID を機械的に付けれる
a. カラム有無を考えなくて良い
2. JOIN 時にも TenantID を機械的に付けれる
3. 子テーブルから自テナントデータのみを取得可能
4. 親, 子テーブルで異なる TenantID のデータを排除
5. シャーディングが必要になった際, キーに使える
テーブルスキーマ
ID TenantID Name
1 A Foo
2 B Bar
parents
ID ParentID TenantID Name
11 1 A Foo
22 2 B Bar
children
© 2022 LayerX Inc.
18
JOIN 時にも TenantID を機械的に付けれる
通常, 起きてはいけない不整合データを取得時に排除可能.
※ INSERT時に検知すべきかつ, 従来のリレーショナルモデル
では考慮しなくてもよい問題ではある...
例.リレーションのあるテーブル間で異なる TenantID のレコード
テーブルスキーマ
ID TenantID Name
1 A Foo
2 B Bar
parents
ID ParentID TenantID Name
11 1 A Foo
22 2 XXX Bar
children
SELECT *
FROM parents
INNER JOIN children ON parents.ID = children.ParentID
AND parents.TenantID = children.TenantID
WHERE parents.TenantID = "B";
© 2022 LayerX Inc.
19
子テーブルから自テナントレコードのみを取得可能
アプリの処理単位もテナント単位なので,
利用しやすいデータモデル.
API 等から ID 指定でリソース取得するケースでも,
機械的に TenantID を付けてバリデーションできる.
テーブルスキーマ
ID TenantID Name
1 A Foo
2 B Bar
parents
ID ParentID TenantID Name
11 1 A Foo
22 2 B Bar
children
SELECT * FROM children
WHERE TenantID = "B";
SELECT * FROM children
WHERE TenantID = "B" AND ID IN ("ID1", "ID2", ...);
© 2022 LayerX Inc.
20
シャーディングが必要になった際には, キーとして使用できる.
Google’s F1 paper で, 分散データベースの階層型のデータモデル(The Cluster
Hierarchical Model)が触れられている.
リレーショナルモデルでは, 親テーブルへの外部キーを持つカラムだけでリレーションを表
現可能. しかし, 分散データベース環境化では従来のリレーショナルモデルの外部キーだ
けではトランザクション, ジョインなどのコストが高価になる.
先祖(親)の ID をプライマリキーに含めることで, 物理的にも同じマシンでの処理が行え
る.
● Google F1
● Designing your SaaS Database for Scale with Postgres / Citus Data
● Sharding a multi-tenant app with Postgres / Citus Data
テーブルスキーマ
© 2022 LayerX Inc.
21
従来のリレーショナルモデルとの比較
テーブルスキーマ
© 2022 LayerX Inc.
22
ユニークキーやインデックスも TenantID を軸に設計する
例えば name カラムにユニークキー制約をつけたい場合,
TenantID を先頭(プレフィックス)に付けた複合キー にする.
テーブルスキーマ
CREATE TABLE `table_a` (
`id` int(11) NOT NULL,
`tenant_id` varchar(36) NOT NULL,
`name` varchar(36) NOT NULL,
PRIMARY KEY `id`,
UNIQUE KEY (`tenant_id`,`name`),
CONSTRAINT `fk_tenant_id` FOREIGN KEY
(`tenant_id`) REFERENCES `tenants` (`id`)
) ENGINE=InnoDB;
ID TenantID Name
1 A Foo
2 B Bar
© 2022 LayerX Inc.
23
複合キーは列挙したカラム順に連結して, 内部値としてインデックスされる.
WHERE tenant_id = “B” (インデックスのプレフィックス) だけでも既知の範囲に絞るイ
ンデックスとして機能する.
アプリが使用するテナントのレコードへの効率的なクエリになる.
PrimaryKey を用いないクエリでは, 必須のインデックスになる.
※ PrimaryKey を用いる場合も, 後述する Context が持つ TenantID と一致している
か検証するため, WHERE 条件として使用している
tips:複合キーのカラムを ForeignKey として使用する際の話
MySQL 外部キー制約とインデックスに必要な知識 - LayerX エンジニアブログ
テーブルスキーマ
© 2022 LayerX Inc.
24
A defined type in Go
TenantID という型を新たに定義して使用
underlying type は, プリミティブな string 型
A defined type の主なメリット
● 関数引数, 返り値の間違いや代入ミスをコンパイル時にエラーにできる
● 独自メソッドを実装できる
型(Type)の実装
type TenantID string
© 2022 LayerX Inc.
25
Generate go model from a database table schema
テーブル定義から, Go の Struct 生成を行う.
xo というツールで自動生成.
TenantID カラム生成時には, 独自定義した型を使用している.
型(Type)の実装
type TableA struct {
ID string
TenantID TenantID
Name string
}
© 2022 LayerX Inc.
26
リクエストを受け取ったAPIサーバーは, コンテキストに TenantID を入れる
ユーザーの認証後に特定されたテナント情報をアプリのMiddleware層でセット.
以降は, この TenantID をもとにレスポンスまで処理を行っていく.
コンテキスト(Request-scoped Data)
func ContextWithTenantID(ctx context.Context, tenantID TenantID) context.Context {
return context.WithValue(ctx, ctxKeyTenantID, tenantID)
}
© 2022 LayerX Inc.
27
バッチ処理などの実装
テナント毎に処理を実行するように関数を実装する
※ テナントをまたいで処理を行う関数を極力実装しない
コンテキスト(Request-scoped Data)
func ProcessPerTenant(ctx context.Context, input string) context.Context {
// do stuff…
}
© 2022 LayerX Inc.
28
ORM
gorm の Callback plugin を実装.
WHERE 句に自動で TenantID 条件がつくようにしている.
func NewGlobalDB(conf *mysql.Config) {
…
// Register callback functions
db.Callback().Query().Before("gorm:query").Register("my_plugin:before_query", callbackTenantID)
db.Callback().Update().Before("gorm:update").Register("my_plugin:before_update", callbackTenantID)
db.Callback().Delete().Before("gorm:delete").Register("my_plugin:before_delete", callbackTenantID)
db.Callback().RowQuery().Before("gorm:row_query").Register("my_plugin:before_row_query", callbackTenantID)
}
© 2022 LayerX Inc.
29
ORM
登録する Callback 関数の実装は,
Struct 内の TenantID フィールドを検出して, WHERE 句をセットする.
(SELECT / CREATE / UPDATE / DELETE)
アプリから呼び出す際には, TenantID を引数に渡してコールバック関数が登録された
DBインスタンスを仕様する.
err := app.NewDB(db, tenantID).First(&model).Error
© 2022 LayerX Inc.
30
一部のメソッド(Raw, Exec)では, callback 関数では対応出来ない..
※ Raw, Exec は, 記述したSQLをそのまま実行できる機能
● 基本的に使用させない.(CIでの検知)
● 管理画面など, 例外的には使用可能
ORM
var result Result
err := db.Raw("SELECT id, name, age FROM users WHERE name = ?", 3).Scan(&result)
© 2022 LayerX Inc.
31
callback 関数が発火せずに実行されたクエリに対しては,
ログを出力するようにもしている.
意図せずに, TenantID を指定しないクエリを発見出来るようにする.
ORM
© 2022 LayerX Inc.
32
バリデーション
APIサーバーの終端でレスポンスデータのバリデーションを実行
RDS含め Redis, ElasticSearch, DynamoDB などデータソース全体で,
他のテナントデータが含まれていないことを検証する最後の砦.
関数内で再帰的にレスポンスオブジェクトの TenantID フィールドが一致していることを
検証.
Go で書くには骨が折れる reflect での処理.
func ValidateTenantID(tenantID TenantID, obj interface{}) {
// do validation…
}
© 2022 LayerX Inc.
33
ログには常に TenantID を含めて出力
logger 側で Context 内の TenantID 自動で出力するように実装
アプリからの呼び出し:
モニタリングツール(Datadog)上でも,
Log Facets としてフィルタも可能.
トラブルシュート時にデータソース特定や
カスタマー連絡に役立ちます.
ロギング / モニタリング
app.LogError(ctx, err).Send()
© 2022 LayerX Inc.
34
TenantID がついていないSQLクエリログをモニタリングツールで監視
正規表現でのチェックが必要なため, まだまだ調整中
(理想はSQLパーサーでアラート条件を作れること)
ロギング / モニタリング
© 2022 LayerX Inc.
35
単体テスト毎にテナントを作成して, 並列に実行する
● 他テナントデータに影響を与える場合, テストで検知出来る
○ 他のテナントへ相互に影響を与える機能が存在しない
● パッケージ内の全テスト完了まで, テストデータをドロップ(削除)しない
● テスト/サブテスト並列度を上げる動機づけになる
● テナントセットアップは, ヘルパー関数を用意
テスト
func TestA(t *testing.T) {
helper.SetupTenant(t)
t.Run(“Case1”, func(t *testing.T) {
t.Parallel(t)
// run tests…
}
}
© 2022 LayerX Inc.
36
● アプリケーション実装でのテナントデータの論理的な分離を行う方針を取っていま
す.
○ データベースやミドルウェアでの制御でも, ポリシー × ユーザーでのロジック
実装部分は避けられないと思います.
● 開発/テスト/デプロイのコストの低さやロックインを避けれるなどメリットはあります.
○ その反面, アプリ側でのガードレール実装の重要度は高くなります.
● 安全かつ開発スピードも落ちない仕組みの改善は進めていきます.
まとめ

Contenu connexe

Tendances

AWSのログ管理ベストプラクティス
AWSのログ管理ベストプラクティスAWSのログ管理ベストプラクティス
AWSのログ管理ベストプラクティスAkihiro Kuwano
 
Mercari JPのモノリスサービスをKubernetesに移行した話 PHP Conference 2022 9/24
Mercari JPのモノリスサービスをKubernetesに移行した話 PHP Conference 2022 9/24Mercari JPのモノリスサービスをKubernetesに移行した話 PHP Conference 2022 9/24
Mercari JPのモノリスサービスをKubernetesに移行した話 PHP Conference 2022 9/24Shin Ohno
 
こんなに使える!今どきのAPIドキュメンテーションツール
こんなに使える!今どきのAPIドキュメンテーションツールこんなに使える!今どきのAPIドキュメンテーションツール
こんなに使える!今どきのAPIドキュメンテーションツールdcubeio
 
マイクロサービス 4つの分割アプローチ
マイクロサービス 4つの分割アプローチマイクロサービス 4つの分割アプローチ
マイクロサービス 4つの分割アプローチ増田 亨
 
イミュータブルデータモデル(入門編)
イミュータブルデータモデル(入門編)イミュータブルデータモデル(入門編)
イミュータブルデータモデル(入門編)Yoshitaka Kawashima
 
世界一わかりやすいClean Architecture
世界一わかりやすいClean Architecture世界一わかりやすいClean Architecture
世界一わかりやすいClean ArchitectureAtsushi Nakamura
 
DDD x CQRS 更新系と参照系で異なるORMを併用して上手くいった話
DDD x CQRS   更新系と参照系で異なるORMを併用して上手くいった話DDD x CQRS   更新系と参照系で異なるORMを併用して上手くいった話
DDD x CQRS 更新系と参照系で異なるORMを併用して上手くいった話Koichiro Matsuoka
 
乗っ取れコンテナ!!開発者から見たコンテナセキュリティの考え方(CloudNative Days Tokyo 2021 発表資料)
乗っ取れコンテナ!!開発者から見たコンテナセキュリティの考え方(CloudNative Days Tokyo 2021 発表資料)乗っ取れコンテナ!!開発者から見たコンテナセキュリティの考え方(CloudNative Days Tokyo 2021 発表資料)
乗っ取れコンテナ!!開発者から見たコンテナセキュリティの考え方(CloudNative Days Tokyo 2021 発表資料)NTT DATA Technology & Innovation
 
開発速度が速い #とは(LayerX社内資料)
開発速度が速い #とは(LayerX社内資料)開発速度が速い #とは(LayerX社内資料)
開発速度が速い #とは(LayerX社内資料)mosa siru
 
Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える
Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考えるGoのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える
Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考えるpospome
 
君はyarn.lockをコミットしているか?
君はyarn.lockをコミットしているか?君はyarn.lockをコミットしているか?
君はyarn.lockをコミットしているか?Teppei Sato
 
なぜ、いま リレーショナルモデルなのか(理論から学ぶデータベース実践入門読書会スペシャル)
なぜ、いま リレーショナルモデルなのか(理論から学ぶデータベース実践入門読書会スペシャル)なぜ、いま リレーショナルモデルなのか(理論から学ぶデータベース実践入門読書会スペシャル)
なぜ、いま リレーショナルモデルなのか(理論から学ぶデータベース実践入門読書会スペシャル)Mikiya Okuno
 
DBスキーマもバージョン管理したい!
DBスキーマもバージョン管理したい!DBスキーマもバージョン管理したい!
DBスキーマもバージョン管理したい!kwatch
 
[AWS EXpert Online for JAWS-UG 18] 見せてやるよ、Step Functions の本気ってやつをな
[AWS EXpert Online for JAWS-UG 18] 見せてやるよ、Step Functions の本気ってやつをな[AWS EXpert Online for JAWS-UG 18] 見せてやるよ、Step Functions の本気ってやつをな
[AWS EXpert Online for JAWS-UG 18] 見せてやるよ、Step Functions の本気ってやつをなAmazon Web Services Japan
 
Spring Boot × Vue.jsでSPAを作る
Spring Boot × Vue.jsでSPAを作るSpring Boot × Vue.jsでSPAを作る
Spring Boot × Vue.jsでSPAを作るGo Miyasaka
 
Python におけるドメイン駆動設計(戦術面)の勘どころ
Python におけるドメイン駆動設計(戦術面)の勘どころPython におけるドメイン駆動設計(戦術面)の勘どころ
Python におけるドメイン駆動設計(戦術面)の勘どころJunya Hayashi
 
データ履歴管理のためのテンポラルデータモデルとReladomoの紹介 #jjug_ccc #ccc_g3
データ履歴管理のためのテンポラルデータモデルとReladomoの紹介 #jjug_ccc #ccc_g3 データ履歴管理のためのテンポラルデータモデルとReladomoの紹介 #jjug_ccc #ccc_g3
データ履歴管理のためのテンポラルデータモデルとReladomoの紹介 #jjug_ccc #ccc_g3 Hiroshi Ito
 
初心者向けMongoDBのキホン!
初心者向けMongoDBのキホン!初心者向けMongoDBのキホン!
初心者向けMongoDBのキホン!Tetsutaro Watanabe
 
モノリスからマイクロサービスへの移行 ~ストラングラーパターンの検証~(Spring Fest 2020講演資料)
モノリスからマイクロサービスへの移行 ~ストラングラーパターンの検証~(Spring Fest 2020講演資料)モノリスからマイクロサービスへの移行 ~ストラングラーパターンの検証~(Spring Fest 2020講演資料)
モノリスからマイクロサービスへの移行 ~ストラングラーパターンの検証~(Spring Fest 2020講演資料)NTT DATA Technology & Innovation
 

Tendances (20)

AWSのログ管理ベストプラクティス
AWSのログ管理ベストプラクティスAWSのログ管理ベストプラクティス
AWSのログ管理ベストプラクティス
 
Mercari JPのモノリスサービスをKubernetesに移行した話 PHP Conference 2022 9/24
Mercari JPのモノリスサービスをKubernetesに移行した話 PHP Conference 2022 9/24Mercari JPのモノリスサービスをKubernetesに移行した話 PHP Conference 2022 9/24
Mercari JPのモノリスサービスをKubernetesに移行した話 PHP Conference 2022 9/24
 
こんなに使える!今どきのAPIドキュメンテーションツール
こんなに使える!今どきのAPIドキュメンテーションツールこんなに使える!今どきのAPIドキュメンテーションツール
こんなに使える!今どきのAPIドキュメンテーションツール
 
マイクロサービス 4つの分割アプローチ
マイクロサービス 4つの分割アプローチマイクロサービス 4つの分割アプローチ
マイクロサービス 4つの分割アプローチ
 
イミュータブルデータモデル(入門編)
イミュータブルデータモデル(入門編)イミュータブルデータモデル(入門編)
イミュータブルデータモデル(入門編)
 
世界一わかりやすいClean Architecture
世界一わかりやすいClean Architecture世界一わかりやすいClean Architecture
世界一わかりやすいClean Architecture
 
DDD x CQRS 更新系と参照系で異なるORMを併用して上手くいった話
DDD x CQRS   更新系と参照系で異なるORMを併用して上手くいった話DDD x CQRS   更新系と参照系で異なるORMを併用して上手くいった話
DDD x CQRS 更新系と参照系で異なるORMを併用して上手くいった話
 
乗っ取れコンテナ!!開発者から見たコンテナセキュリティの考え方(CloudNative Days Tokyo 2021 発表資料)
乗っ取れコンテナ!!開発者から見たコンテナセキュリティの考え方(CloudNative Days Tokyo 2021 発表資料)乗っ取れコンテナ!!開発者から見たコンテナセキュリティの考え方(CloudNative Days Tokyo 2021 発表資料)
乗っ取れコンテナ!!開発者から見たコンテナセキュリティの考え方(CloudNative Days Tokyo 2021 発表資料)
 
開発速度が速い #とは(LayerX社内資料)
開発速度が速い #とは(LayerX社内資料)開発速度が速い #とは(LayerX社内資料)
開発速度が速い #とは(LayerX社内資料)
 
Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える
Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考えるGoのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える
Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える
 
君はyarn.lockをコミットしているか?
君はyarn.lockをコミットしているか?君はyarn.lockをコミットしているか?
君はyarn.lockをコミットしているか?
 
なぜ、いま リレーショナルモデルなのか(理論から学ぶデータベース実践入門読書会スペシャル)
なぜ、いま リレーショナルモデルなのか(理論から学ぶデータベース実践入門読書会スペシャル)なぜ、いま リレーショナルモデルなのか(理論から学ぶデータベース実践入門読書会スペシャル)
なぜ、いま リレーショナルモデルなのか(理論から学ぶデータベース実践入門読書会スペシャル)
 
DBスキーマもバージョン管理したい!
DBスキーマもバージョン管理したい!DBスキーマもバージョン管理したい!
DBスキーマもバージョン管理したい!
 
[AWS EXpert Online for JAWS-UG 18] 見せてやるよ、Step Functions の本気ってやつをな
[AWS EXpert Online for JAWS-UG 18] 見せてやるよ、Step Functions の本気ってやつをな[AWS EXpert Online for JAWS-UG 18] 見せてやるよ、Step Functions の本気ってやつをな
[AWS EXpert Online for JAWS-UG 18] 見せてやるよ、Step Functions の本気ってやつをな
 
Spring Boot × Vue.jsでSPAを作る
Spring Boot × Vue.jsでSPAを作るSpring Boot × Vue.jsでSPAを作る
Spring Boot × Vue.jsでSPAを作る
 
Docker Compose 徹底解説
Docker Compose 徹底解説Docker Compose 徹底解説
Docker Compose 徹底解説
 
Python におけるドメイン駆動設計(戦術面)の勘どころ
Python におけるドメイン駆動設計(戦術面)の勘どころPython におけるドメイン駆動設計(戦術面)の勘どころ
Python におけるドメイン駆動設計(戦術面)の勘どころ
 
データ履歴管理のためのテンポラルデータモデルとReladomoの紹介 #jjug_ccc #ccc_g3
データ履歴管理のためのテンポラルデータモデルとReladomoの紹介 #jjug_ccc #ccc_g3 データ履歴管理のためのテンポラルデータモデルとReladomoの紹介 #jjug_ccc #ccc_g3
データ履歴管理のためのテンポラルデータモデルとReladomoの紹介 #jjug_ccc #ccc_g3
 
初心者向けMongoDBのキホン!
初心者向けMongoDBのキホン!初心者向けMongoDBのキホン!
初心者向けMongoDBのキホン!
 
モノリスからマイクロサービスへの移行 ~ストラングラーパターンの検証~(Spring Fest 2020講演資料)
モノリスからマイクロサービスへの移行 ~ストラングラーパターンの検証~(Spring Fest 2020講演資料)モノリスからマイクロサービスへの移行 ~ストラングラーパターンの検証~(Spring Fest 2020講演資料)
モノリスからマイクロサービスへの移行 ~ストラングラーパターンの検証~(Spring Fest 2020講演資料)
 

Similaire à マルチテナントのアプリケーション実装〜実践編〜

Dalvikバイトコードリファレンスの読み方 改訂版
Dalvikバイトコードリファレンスの読み方 改訂版Dalvikバイトコードリファレンスの読み方 改訂版
Dalvikバイトコードリファレンスの読み方 改訂版Takuya Matsunaga
 
[db tech showcase Tokyo 2015] A26:内部犯行による漏えいを防ぐPostgreSQLの透過的暗号化機能に関する実装と利用方法...
[db tech showcase Tokyo 2015] A26:内部犯行による漏えいを防ぐPostgreSQLの透過的暗号化機能に関する実装と利用方法...[db tech showcase Tokyo 2015] A26:内部犯行による漏えいを防ぐPostgreSQLの透過的暗号化機能に関する実装と利用方法...
[db tech showcase Tokyo 2015] A26:内部犯行による漏えいを防ぐPostgreSQLの透過的暗号化機能に関する実装と利用方法...Insight Technology, Inc.
 
Tech Dojo 02/09 IBM Japan CSM
Tech Dojo 02/09 IBM Japan CSMTech Dojo 02/09 IBM Japan CSM
Tech Dojo 02/09 IBM Japan CSM勇 黒沢
 
[db tech showcase Tokyo 2015] D35:高トランザクションを実現するスケーラブルRDBMS技術 by 日本電気株式会社 並木悠太
[db tech showcase Tokyo 2015] D35:高トランザクションを実現するスケーラブルRDBMS技術 by 日本電気株式会社 並木悠太[db tech showcase Tokyo 2015] D35:高トランザクションを実現するスケーラブルRDBMS技術 by 日本電気株式会社 並木悠太
[db tech showcase Tokyo 2015] D35:高トランザクションを実現するスケーラブルRDBMS技術 by 日本電気株式会社 並木悠太Insight Technology, Inc.
 
20151029 ヒカラボ講演資料
20151029 ヒカラボ講演資料20151029 ヒカラボ講演資料
20151029 ヒカラボ講演資料Daisuke Ando
 
株式会社waja 安藤様 登壇資料
株式会社waja 安藤様 登壇資料株式会社waja 安藤様 登壇資料
株式会社waja 安藤様 登壇資料leverages_event
 
復習も兼ねて!C#6.0-7.0
復習も兼ねて!C#6.0-7.0復習も兼ねて!C#6.0-7.0
復習も兼ねて!C#6.0-7.0Yuta Matsumura
 
Node.jsアプリの開発をモダン化するために取り組んできたこと
Node.jsアプリの開発をモダン化するために取り組んできたことNode.jsアプリの開発をモダン化するために取り組んできたこと
Node.jsアプリの開発をモダン化するために取り組んできたことbitbank, Inc. Tokyo, Japan
 
The Twelve-Factor Appで考えるAWSのサービス開発
The Twelve-Factor Appで考えるAWSのサービス開発The Twelve-Factor Appで考えるAWSのサービス開発
The Twelve-Factor Appで考えるAWSのサービス開発Amazon Web Services Japan
 
技術選択とアーキテクトの役割
技術選択とアーキテクトの役割技術選択とアーキテクトの役割
技術選択とアーキテクトの役割Toru Yamaguchi
 
Cosmos DB 入門 multi model multi API編
Cosmos DB 入門 multi model multi API編Cosmos DB 入門 multi model multi API編
Cosmos DB 入門 multi model multi API編Takekazu Omi
 
CSI Driverを開発し自社プライベートクラウドにより適した安全なKubernetes Secrets管理を実現した話
CSI Driverを開発し自社プライベートクラウドにより適した安全なKubernetes Secrets管理を実現した話CSI Driverを開発し自社プライベートクラウドにより適した安全なKubernetes Secrets管理を実現した話
CSI Driverを開発し自社プライベートクラウドにより適した安全なKubernetes Secrets管理を実現した話Katsuya Yamaguchi
 
『これからの.NETアプリケーション開発』セミナー .NET用アプリケーション フレームワーク Open 棟梁 概説
『これからの.NETアプリケーション開発』セミナー .NET用アプリケーション フレームワーク Open 棟梁 概説『これからの.NETアプリケーション開発』セミナー .NET用アプリケーション フレームワーク Open 棟梁 概説
『これからの.NETアプリケーション開発』セミナー .NET用アプリケーション フレームワーク Open 棟梁 概説Daisuke Nishino
 
Mvc conf session_2_shibamura
Mvc conf session_2_shibamuraMvc conf session_2_shibamura
Mvc conf session_2_shibamuraHiroshi Okunushi
 
Prometheus超基礎公開用.pdf
Prometheus超基礎公開用.pdfPrometheus超基礎公開用.pdf
Prometheus超基礎公開用.pdf勇 黒沢
 
Azure でサーバーレス、 Infrastructure as Code どうしてますか?
Azure でサーバーレス、 Infrastructure as Code どうしてますか?Azure でサーバーレス、 Infrastructure as Code どうしてますか?
Azure でサーバーレス、 Infrastructure as Code どうしてますか?Kazumi IWANAGA
 
AWS Dev Day Tokyo 2018 | Amazon DynamoDB Backed な テレコムコアシステムを構築・運用してる話
AWS Dev Day Tokyo 2018 | Amazon DynamoDB Backed なテレコムコアシステムを構築・運用してる話AWS Dev Day Tokyo 2018 | Amazon DynamoDB Backed なテレコムコアシステムを構築・運用してる話
AWS Dev Day Tokyo 2018 | Amazon DynamoDB Backed な テレコムコアシステムを構築・運用してる話SORACOM,INC
 
QuickDemo HashiCorp Terraform with Microsoft Azure and VMware vSphere
QuickDemo HashiCorp Terraform with Microsoft Azure and VMware vSphereQuickDemo HashiCorp Terraform with Microsoft Azure and VMware vSphere
QuickDemo HashiCorp Terraform with Microsoft Azure and VMware vSphereWataru Unno
 
Software Development with Symfony
Software Development with SymfonySoftware Development with Symfony
Software Development with SymfonyAtsuhiro Kubo
 
【C++BUILDER STARTER チュートリアルシリーズ】シーズン2 C++Builderの部 第5回 ‟配列と構造体„
【C++BUILDER STARTER チュートリアルシリーズ】シーズン2 C++Builderの部 第5回 ‟配列と構造体„【C++BUILDER STARTER チュートリアルシリーズ】シーズン2 C++Builderの部 第5回 ‟配列と構造体„
【C++BUILDER STARTER チュートリアルシリーズ】シーズン2 C++Builderの部 第5回 ‟配列と構造体„和弘 井之上
 

Similaire à マルチテナントのアプリケーション実装〜実践編〜 (20)

Dalvikバイトコードリファレンスの読み方 改訂版
Dalvikバイトコードリファレンスの読み方 改訂版Dalvikバイトコードリファレンスの読み方 改訂版
Dalvikバイトコードリファレンスの読み方 改訂版
 
[db tech showcase Tokyo 2015] A26:内部犯行による漏えいを防ぐPostgreSQLの透過的暗号化機能に関する実装と利用方法...
[db tech showcase Tokyo 2015] A26:内部犯行による漏えいを防ぐPostgreSQLの透過的暗号化機能に関する実装と利用方法...[db tech showcase Tokyo 2015] A26:内部犯行による漏えいを防ぐPostgreSQLの透過的暗号化機能に関する実装と利用方法...
[db tech showcase Tokyo 2015] A26:内部犯行による漏えいを防ぐPostgreSQLの透過的暗号化機能に関する実装と利用方法...
 
Tech Dojo 02/09 IBM Japan CSM
Tech Dojo 02/09 IBM Japan CSMTech Dojo 02/09 IBM Japan CSM
Tech Dojo 02/09 IBM Japan CSM
 
[db tech showcase Tokyo 2015] D35:高トランザクションを実現するスケーラブルRDBMS技術 by 日本電気株式会社 並木悠太
[db tech showcase Tokyo 2015] D35:高トランザクションを実現するスケーラブルRDBMS技術 by 日本電気株式会社 並木悠太[db tech showcase Tokyo 2015] D35:高トランザクションを実現するスケーラブルRDBMS技術 by 日本電気株式会社 並木悠太
[db tech showcase Tokyo 2015] D35:高トランザクションを実現するスケーラブルRDBMS技術 by 日本電気株式会社 並木悠太
 
20151029 ヒカラボ講演資料
20151029 ヒカラボ講演資料20151029 ヒカラボ講演資料
20151029 ヒカラボ講演資料
 
株式会社waja 安藤様 登壇資料
株式会社waja 安藤様 登壇資料株式会社waja 安藤様 登壇資料
株式会社waja 安藤様 登壇資料
 
復習も兼ねて!C#6.0-7.0
復習も兼ねて!C#6.0-7.0復習も兼ねて!C#6.0-7.0
復習も兼ねて!C#6.0-7.0
 
Node.jsアプリの開発をモダン化するために取り組んできたこと
Node.jsアプリの開発をモダン化するために取り組んできたことNode.jsアプリの開発をモダン化するために取り組んできたこと
Node.jsアプリの開発をモダン化するために取り組んできたこと
 
The Twelve-Factor Appで考えるAWSのサービス開発
The Twelve-Factor Appで考えるAWSのサービス開発The Twelve-Factor Appで考えるAWSのサービス開発
The Twelve-Factor Appで考えるAWSのサービス開発
 
技術選択とアーキテクトの役割
技術選択とアーキテクトの役割技術選択とアーキテクトの役割
技術選択とアーキテクトの役割
 
Cosmos DB 入門 multi model multi API編
Cosmos DB 入門 multi model multi API編Cosmos DB 入門 multi model multi API編
Cosmos DB 入門 multi model multi API編
 
CSI Driverを開発し自社プライベートクラウドにより適した安全なKubernetes Secrets管理を実現した話
CSI Driverを開発し自社プライベートクラウドにより適した安全なKubernetes Secrets管理を実現した話CSI Driverを開発し自社プライベートクラウドにより適した安全なKubernetes Secrets管理を実現した話
CSI Driverを開発し自社プライベートクラウドにより適した安全なKubernetes Secrets管理を実現した話
 
『これからの.NETアプリケーション開発』セミナー .NET用アプリケーション フレームワーク Open 棟梁 概説
『これからの.NETアプリケーション開発』セミナー .NET用アプリケーション フレームワーク Open 棟梁 概説『これからの.NETアプリケーション開発』セミナー .NET用アプリケーション フレームワーク Open 棟梁 概説
『これからの.NETアプリケーション開発』セミナー .NET用アプリケーション フレームワーク Open 棟梁 概説
 
Mvc conf session_2_shibamura
Mvc conf session_2_shibamuraMvc conf session_2_shibamura
Mvc conf session_2_shibamura
 
Prometheus超基礎公開用.pdf
Prometheus超基礎公開用.pdfPrometheus超基礎公開用.pdf
Prometheus超基礎公開用.pdf
 
Azure でサーバーレス、 Infrastructure as Code どうしてますか?
Azure でサーバーレス、 Infrastructure as Code どうしてますか?Azure でサーバーレス、 Infrastructure as Code どうしてますか?
Azure でサーバーレス、 Infrastructure as Code どうしてますか?
 
AWS Dev Day Tokyo 2018 | Amazon DynamoDB Backed な テレコムコアシステムを構築・運用してる話
AWS Dev Day Tokyo 2018 | Amazon DynamoDB Backed なテレコムコアシステムを構築・運用してる話AWS Dev Day Tokyo 2018 | Amazon DynamoDB Backed なテレコムコアシステムを構築・運用してる話
AWS Dev Day Tokyo 2018 | Amazon DynamoDB Backed な テレコムコアシステムを構築・運用してる話
 
QuickDemo HashiCorp Terraform with Microsoft Azure and VMware vSphere
QuickDemo HashiCorp Terraform with Microsoft Azure and VMware vSphereQuickDemo HashiCorp Terraform with Microsoft Azure and VMware vSphere
QuickDemo HashiCorp Terraform with Microsoft Azure and VMware vSphere
 
Software Development with Symfony
Software Development with SymfonySoftware Development with Symfony
Software Development with Symfony
 
【C++BUILDER STARTER チュートリアルシリーズ】シーズン2 C++Builderの部 第5回 ‟配列と構造体„
【C++BUILDER STARTER チュートリアルシリーズ】シーズン2 C++Builderの部 第5回 ‟配列と構造体„【C++BUILDER STARTER チュートリアルシリーズ】シーズン2 C++Builderの部 第5回 ‟配列と構造体„
【C++BUILDER STARTER チュートリアルシリーズ】シーズン2 C++Builderの部 第5回 ‟配列と構造体„
 

マルチテナントのアプリケーション実装〜実践編〜

  • 1. © 2022 LayerX Inc. 1 マルチテナントのアプリケーション実装 〜実践編〜 SaaSにおけるマルチテナント設計の悩みと勘所 SaaS.tech #2 2022.04.20
  • 2. © 2022 LayerX Inc. 2 自己紹介 株式会社LayerX 中川佳希 @yyoshiki41 バクラク請求書のテックリード バックエンドからフロントエンドまで. SaaS が扱う業務ドメインへの好奇心と サービスの成長に日々ワクワクを感じています. Gopher.
  • 3. © 2022 LayerX Inc. 3 1. 導入 a. マルチテナントSaaS 2. データベース設計 a. 設計パターン 3. アプリケーション側での実装 a. テーブルスキーマ b. 型(Type)の実装 c. コンテキスト(Request-scoped Data) d. ORM e. バリデーション f. ロギング / モニタリング g. テスト 章立て
  • 4. © 2022 LayerX Inc. 4 マルチテナント SaaS マルチテナント SaaS は・・・ 同質のソフトウェアを全テナントのユーザーへ提供. リソースの一部またはすべてをテナント間で共有. SaaS 提供側のメリット(シングルテナントアプリケーションと比較) ● 機能提供アウトカムの最大化 ● サービス運用の効率化 ● インフラコストの削減
  • 5. © 2022 LayerX Inc. 5 開発者を悩ますポイント 1. テナント毎に安全にデータを分離した状態で、アプリケーションを実装できるか? 2. テナント境界線をどのレイヤで実装するか? 3. テナント間のシステムリソース共有をどこまで行うか? マルチテナント SaaS が絶対に防ぐべきこと => データが他のテナントにも共有されてしまうこと(漏洩) マルチテナント SaaS
  • 6. © 2022 LayerX Inc. 6 マルチテナント SaaS での安全なデータ分離の実装 安全にテナント毎のデータを分離するには? ● データベース ● ミドルウェア ○ アプリ => ミドルウェア => データベース のような Proxy を想定 ● アプリケーション ● テスト 以降では低レイヤの部分から順にみていきます.
  • 7. © 2022 LayerX Inc. 7 SaaS 開発における最初の分岐点 1. データベースをテナント毎に作成 => データ所有者(テナント)を データベース名 で表現 2. データベースは共有、スキーマをテナント毎に作成 => データ所有者(テナント)を スキーマ で表現 3. データベースは共有、テーブルをテナント毎に作成 => データ所有者(テナント)を テーブル名 で表現 4. データベースは共有、テーブルも共有、 各テーブルがテナント識別カラムを持ち、レコード値で識別・分離 => データ所有者(テナント)を テーブルのカラム値 で表現 データベース設計 Isolated Shared
  • 8. © 2022 LayerX Inc. 8 1. データベースをテナント毎に作成 メリット 1. データの持ち方としては、もっとも堅牢 デメリット 1. マイグレーション(テーブルスキーマ変更)コスト 2. アプリからのデータベース接続コスト a. データベースユーザーが異なる場合, セッションも異なる 3. テナント作成時に、毎回データベースの初期化処理が必要 a. テーブル作成、データベースユーザーの認証設定 4. テナントとデータベースのリレーションが別途必要 Tenant B Tenant A
  • 9. © 2022 LayerX Inc. 9 スキーマとは... PostgreSQL などで名前空間を作成できる仕組み. データベースオブジェクト(テーブル、関数など)を 同じオブジェクト名でもスキーマが異なれば作成可能になる. MySQL ではオブジェクトを論理的に分ける (グルーピングする)仕組みに相当するものは見当たらず. 2. データベースは共有、スキーマをテナント毎に作成 Global Tables Tenant A Tables Tenant B
  • 10. © 2022 LayerX Inc. 10 2. データベースは共有、スキーマをテナント毎に作成 Global Tables Tenant A Tables Tenant B メリット 1. スキーマ単位で所有者を決めれる デメリット 1. マイグレーション(テーブルスキーマ変更)コスト 2. アプリからのデータベース接続コスト a. スキーマ所有者が異なる場合, セッションも異なる 3. テナント作成時に、毎回スキーマの初期化処理が必要 a. テーブル作成、スキーマ所有者の認証設定 4. テナントとスキーマのリレーションが別途必要
  • 11. © 2022 LayerX Inc. 11 メリット 1. 1つのデータベース内でテナントのデータを保持できる デメリット 1. マイグレーション(テーブルスキーマ変更)コスト 2. テナント作成時に、毎回テーブルの初期化処理が必要 a. テーブル作成 3. テナントとテーブル名のリレーションが別途必要 4. テナントのテーブル間で外部キーが煩雑になる 3. データベースは共有、テーブルをテナント毎に作成 Global Tenant A tables Tenant B tables
  • 12. © 2022 LayerX Inc. 12 ID TenantID Name 1 A Foo 2 B Bar メリット 1. マイグレーション(テーブルスキーマ変更)コストが低い 2. データベース側での設定コストが低い デメリット 1. 他テナントのレコードへアクセスが容易 a. 同一テーブルの為, 最もカジュアルにアクセス可能 2. ロジックを実装する必要がある a. アプリ側で制御する場合 i. WHERE 句 b. データベース側で制御する場合 i. Row-Level Security での制御 ii. ストアドプロシージャを実装しての制御 4. テーブルにテナント識別カラムを持ち, 行単位で制御 Global tables
  • 13. © 2022 LayerX Inc. 13 テーブルに識別カラムを持ち, テーブルへのポリシー × データベースユーザーで制御するをデータベース側で実装す る例 データベースユーザーのセッション管理などが一定ネックになる. ● PostgreSQL Row Level Security ○ PostgreSQL ネイティブの機能 ● Implementing row level security in MySQL / SQL Maestro ○ Trigger や View テーブルを使って, MySQL で RLS を実現する例 4. テーブルにテナント識別カラムを持ち, 行単位で制御
  • 14. © 2022 LayerX Inc. 14 アプリ => ミドルウェア => データベース のようなプロキシを想定. ● ProxySQL ○ mysql_query_rules: WHERE 句に TenantID がないクエリをはじく ● MariaDB MaxScale ○ Deny ルールを作る: WHERE 句に TenantID がないクエリをはじく 上記のような SQL を解釈できるプロキシミドルウェアで, ポリシー(とデータベースユーザーの掛け合わせ)での制御. ポリシーに沿わないレコードへのアクセスを拒否する. 実検証までは行っておらず 🙏 ミドルウェア(+α)
  • 15. © 2022 LayerX Inc. 15 4. テーブルにテナント識別カラムを持ち, 行単位で制御 主な理由 ● データベース、マイグレーション運用コストが最も低い ○ デプロイ(マイグレーション)難易度が高いサービスは致命的 ○ テナント追加時, データベース側の初期化処理のサブシステム等が不要 ● データベース側にロジックを寄せることでのロックインを避けたい ○ ストアドプロシージャなどの実装への依存も持ちたくない ● テナント数のスケールに最も適している ○ テナント毎のデータベースユーザーと管理, アプリからの接続セッションの管 理なども不要 ● ロジックのテストの行いやすさ バクラクでのデータベース設計
  • 16. © 2022 LayerX Inc. 16 4. テーブルにテナント識別カラムを持ち, 行単位で制御 テナント毎にデータを(論理的に)識別・分離して扱うことは, 至上命題. 実際の取り組み(ここからが本題) 1. テーブルスキーマ 2. 型(Type)の実装 3. コンテキスト(Request-scoped Data) 4. ORM 5. バリデーション 6. ロギング / モニタリング 7. テスト アプリケーション実装〜実践編〜 ID TenantID Name 1 A Foo 2 B Bar
  • 17. © 2022 LayerX Inc. 17 全テーブルへ冗長に TenantID (テナント識別カラム)を付ける (中間テーブル、子テーブルでも同様) 親テーブルにはマストで必要. 子テーブルでは従来, 親テーブルへの外部キー制約だけで充分. 冗長にカラムを付ける理由は, 1. ORM などで, WHERE句に TenantID を機械的に付けれる a. カラム有無を考えなくて良い 2. JOIN 時にも TenantID を機械的に付けれる 3. 子テーブルから自テナントデータのみを取得可能 4. 親, 子テーブルで異なる TenantID のデータを排除 5. シャーディングが必要になった際, キーに使える テーブルスキーマ ID TenantID Name 1 A Foo 2 B Bar parents ID ParentID TenantID Name 11 1 A Foo 22 2 B Bar children
  • 18. © 2022 LayerX Inc. 18 JOIN 時にも TenantID を機械的に付けれる 通常, 起きてはいけない不整合データを取得時に排除可能. ※ INSERT時に検知すべきかつ, 従来のリレーショナルモデル では考慮しなくてもよい問題ではある... 例.リレーションのあるテーブル間で異なる TenantID のレコード テーブルスキーマ ID TenantID Name 1 A Foo 2 B Bar parents ID ParentID TenantID Name 11 1 A Foo 22 2 XXX Bar children SELECT * FROM parents INNER JOIN children ON parents.ID = children.ParentID AND parents.TenantID = children.TenantID WHERE parents.TenantID = "B";
  • 19. © 2022 LayerX Inc. 19 子テーブルから自テナントレコードのみを取得可能 アプリの処理単位もテナント単位なので, 利用しやすいデータモデル. API 等から ID 指定でリソース取得するケースでも, 機械的に TenantID を付けてバリデーションできる. テーブルスキーマ ID TenantID Name 1 A Foo 2 B Bar parents ID ParentID TenantID Name 11 1 A Foo 22 2 B Bar children SELECT * FROM children WHERE TenantID = "B"; SELECT * FROM children WHERE TenantID = "B" AND ID IN ("ID1", "ID2", ...);
  • 20. © 2022 LayerX Inc. 20 シャーディングが必要になった際には, キーとして使用できる. Google’s F1 paper で, 分散データベースの階層型のデータモデル(The Cluster Hierarchical Model)が触れられている. リレーショナルモデルでは, 親テーブルへの外部キーを持つカラムだけでリレーションを表 現可能. しかし, 分散データベース環境化では従来のリレーショナルモデルの外部キーだ けではトランザクション, ジョインなどのコストが高価になる. 先祖(親)の ID をプライマリキーに含めることで, 物理的にも同じマシンでの処理が行え る. ● Google F1 ● Designing your SaaS Database for Scale with Postgres / Citus Data ● Sharding a multi-tenant app with Postgres / Citus Data テーブルスキーマ
  • 21. © 2022 LayerX Inc. 21 従来のリレーショナルモデルとの比較 テーブルスキーマ
  • 22. © 2022 LayerX Inc. 22 ユニークキーやインデックスも TenantID を軸に設計する 例えば name カラムにユニークキー制約をつけたい場合, TenantID を先頭(プレフィックス)に付けた複合キー にする. テーブルスキーマ CREATE TABLE `table_a` ( `id` int(11) NOT NULL, `tenant_id` varchar(36) NOT NULL, `name` varchar(36) NOT NULL, PRIMARY KEY `id`, UNIQUE KEY (`tenant_id`,`name`), CONSTRAINT `fk_tenant_id` FOREIGN KEY (`tenant_id`) REFERENCES `tenants` (`id`) ) ENGINE=InnoDB; ID TenantID Name 1 A Foo 2 B Bar
  • 23. © 2022 LayerX Inc. 23 複合キーは列挙したカラム順に連結して, 内部値としてインデックスされる. WHERE tenant_id = “B” (インデックスのプレフィックス) だけでも既知の範囲に絞るイ ンデックスとして機能する. アプリが使用するテナントのレコードへの効率的なクエリになる. PrimaryKey を用いないクエリでは, 必須のインデックスになる. ※ PrimaryKey を用いる場合も, 後述する Context が持つ TenantID と一致している か検証するため, WHERE 条件として使用している tips:複合キーのカラムを ForeignKey として使用する際の話 MySQL 外部キー制約とインデックスに必要な知識 - LayerX エンジニアブログ テーブルスキーマ
  • 24. © 2022 LayerX Inc. 24 A defined type in Go TenantID という型を新たに定義して使用 underlying type は, プリミティブな string 型 A defined type の主なメリット ● 関数引数, 返り値の間違いや代入ミスをコンパイル時にエラーにできる ● 独自メソッドを実装できる 型(Type)の実装 type TenantID string
  • 25. © 2022 LayerX Inc. 25 Generate go model from a database table schema テーブル定義から, Go の Struct 生成を行う. xo というツールで自動生成. TenantID カラム生成時には, 独自定義した型を使用している. 型(Type)の実装 type TableA struct { ID string TenantID TenantID Name string }
  • 26. © 2022 LayerX Inc. 26 リクエストを受け取ったAPIサーバーは, コンテキストに TenantID を入れる ユーザーの認証後に特定されたテナント情報をアプリのMiddleware層でセット. 以降は, この TenantID をもとにレスポンスまで処理を行っていく. コンテキスト(Request-scoped Data) func ContextWithTenantID(ctx context.Context, tenantID TenantID) context.Context { return context.WithValue(ctx, ctxKeyTenantID, tenantID) }
  • 27. © 2022 LayerX Inc. 27 バッチ処理などの実装 テナント毎に処理を実行するように関数を実装する ※ テナントをまたいで処理を行う関数を極力実装しない コンテキスト(Request-scoped Data) func ProcessPerTenant(ctx context.Context, input string) context.Context { // do stuff… }
  • 28. © 2022 LayerX Inc. 28 ORM gorm の Callback plugin を実装. WHERE 句に自動で TenantID 条件がつくようにしている. func NewGlobalDB(conf *mysql.Config) { … // Register callback functions db.Callback().Query().Before("gorm:query").Register("my_plugin:before_query", callbackTenantID) db.Callback().Update().Before("gorm:update").Register("my_plugin:before_update", callbackTenantID) db.Callback().Delete().Before("gorm:delete").Register("my_plugin:before_delete", callbackTenantID) db.Callback().RowQuery().Before("gorm:row_query").Register("my_plugin:before_row_query", callbackTenantID) }
  • 29. © 2022 LayerX Inc. 29 ORM 登録する Callback 関数の実装は, Struct 内の TenantID フィールドを検出して, WHERE 句をセットする. (SELECT / CREATE / UPDATE / DELETE) アプリから呼び出す際には, TenantID を引数に渡してコールバック関数が登録された DBインスタンスを仕様する. err := app.NewDB(db, tenantID).First(&model).Error
  • 30. © 2022 LayerX Inc. 30 一部のメソッド(Raw, Exec)では, callback 関数では対応出来ない.. ※ Raw, Exec は, 記述したSQLをそのまま実行できる機能 ● 基本的に使用させない.(CIでの検知) ● 管理画面など, 例外的には使用可能 ORM var result Result err := db.Raw("SELECT id, name, age FROM users WHERE name = ?", 3).Scan(&result)
  • 31. © 2022 LayerX Inc. 31 callback 関数が発火せずに実行されたクエリに対しては, ログを出力するようにもしている. 意図せずに, TenantID を指定しないクエリを発見出来るようにする. ORM
  • 32. © 2022 LayerX Inc. 32 バリデーション APIサーバーの終端でレスポンスデータのバリデーションを実行 RDS含め Redis, ElasticSearch, DynamoDB などデータソース全体で, 他のテナントデータが含まれていないことを検証する最後の砦. 関数内で再帰的にレスポンスオブジェクトの TenantID フィールドが一致していることを 検証. Go で書くには骨が折れる reflect での処理. func ValidateTenantID(tenantID TenantID, obj interface{}) { // do validation… }
  • 33. © 2022 LayerX Inc. 33 ログには常に TenantID を含めて出力 logger 側で Context 内の TenantID 自動で出力するように実装 アプリからの呼び出し: モニタリングツール(Datadog)上でも, Log Facets としてフィルタも可能. トラブルシュート時にデータソース特定や カスタマー連絡に役立ちます. ロギング / モニタリング app.LogError(ctx, err).Send()
  • 34. © 2022 LayerX Inc. 34 TenantID がついていないSQLクエリログをモニタリングツールで監視 正規表現でのチェックが必要なため, まだまだ調整中 (理想はSQLパーサーでアラート条件を作れること) ロギング / モニタリング
  • 35. © 2022 LayerX Inc. 35 単体テスト毎にテナントを作成して, 並列に実行する ● 他テナントデータに影響を与える場合, テストで検知出来る ○ 他のテナントへ相互に影響を与える機能が存在しない ● パッケージ内の全テスト完了まで, テストデータをドロップ(削除)しない ● テスト/サブテスト並列度を上げる動機づけになる ● テナントセットアップは, ヘルパー関数を用意 テスト func TestA(t *testing.T) { helper.SetupTenant(t) t.Run(“Case1”, func(t *testing.T) { t.Parallel(t) // run tests… } }
  • 36. © 2022 LayerX Inc. 36 ● アプリケーション実装でのテナントデータの論理的な分離を行う方針を取っていま す. ○ データベースやミドルウェアでの制御でも, ポリシー × ユーザーでのロジック 実装部分は避けられないと思います. ● 開発/テスト/デプロイのコストの低さやロックインを避けれるなどメリットはあります. ○ その反面, アプリ側でのガードレール実装の重要度は高くなります. ● 安全かつ開発スピードも落ちない仕組みの改善は進めていきます. まとめ