Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
NestJSで作るマルチテナントSaaS / Multi-tenant NestJS-base...
Search
hiroga
March 18, 2022
Technology
0
990
NestJSで作るマルチテナントSaaS / Multi-tenant NestJS-based SaaS
NestJS meetup Online #1
NestJSで作るマルチテナントSaaS
hiroga
March 18, 2022
Tweet
Share
More Decks by hiroga
See All by hiroga
LlamaGen: LlamaのNext-Token予測を使った画像生成 / Autoregressive Model Beats Diffusion: Llama for Scalable Image Generation
hiroga
0
280
人事評価GPTsで評価の本質に向き合おう! / HR GPTs: Essential evaluations focus!
hiroga
1
380
生成AI元年を個人的に振り返る / Reflecting on First Year of the Generative-AI
hiroga
0
320
AWS Startup Day 2023 今日ここで! コスト削減ハンズオン / Cost-Saving Hands-On today!
hiroga
0
15
ChatGPT社内活用資料 / Internal use of ChatGPT
hiroga
0
90
マルチテナントSaaSのカスタム要件に、 Auth0テナントを分割せず向き合う! / Multi tenant SaaS with Auth0
hiroga
1
2.7k
雑な攻撃からELBを守る一工夫 +おまけ / Know-how to protect servers from miscellaneous attacks
hiroga
0
2.4k
[AWS CDK] 1,000+のCloudWatch Alarmsを自動生成する技術 / [AWS CDK] Technics to Generate 1,000+ CloudWatch Alarms
hiroga
2
1.6k
Coralの投資先にあらゆる保険金請求をWeb化しようとしている会社があるらしいですよ
hiroga
1
120
Other Decks in Technology
See All in Technology
【JAWS-UG大阪 reInvent reCap LT大会 サンバが始まったら強制終了】“1分”で初めてのソロ参戦reInventを数字で振り返りながら反省する
ttelltte
0
140
AWSサービスアップデート 2024/12 Part3
nrinetcom
PRO
0
140
Goで実践するBFP
hiroyaterui
1
120
カップ麺の待ち時間(3分)でわかるPartyRockアップデート
ryutakondo
0
140
2025年の挑戦 コーポレートエンジニアの技術広報/techpr5
nishiuma
0
140
EMConf JP の楽しみ方 / How to enjoy EMConf JP
pauli
2
150
.NET 最新アップデート ~ AI とクラウド時代のアプリモダナイゼーション
chack411
0
200
re:Invent2024 KeynoteのAmazon Q Developer考察
yusukeshimizu
1
150
Git scrapingで始める継続的なデータ追跡 / Git Scraping
ohbarye
5
490
RubyでKubernetesプログラミング
sat
PRO
4
160
シフトライトなテスト活動を適切に行うことで、無理な開発をせず、過剰にテストせず、顧客をビックリさせないプロダクトを作り上げているお話 #RSGT2025 / Shift Right
nihonbuson
3
2.1k
CDKのコードレビューを楽にするパッケージcdk-mentorを作ってみた/cdk-mentor
tomoki10
0
210
Featured
See All Featured
Building Adaptive Systems
keathley
38
2.4k
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
26
1.9k
Into the Great Unknown - MozCon
thekraken
34
1.6k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
33
2.7k
Understanding Cognitive Biases in Performance Measurement
bluesmoon
27
1.5k
KATA
mclloyd
29
14k
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
356
29k
The MySQL Ecosystem @ GitHub 2015
samlambert
250
12k
Speed Design
sergeychernyshev
25
740
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
44
7k
Unsuck your backbone
ammeep
669
57k
Raft: Consensus for Rubyists
vanstee
137
6.7k
Transcript
\ 積極採⽤中 / 2022年3⽉18⽇ ⼩笠原寛明(@xhiroga) NestJSで作るマルチテナントSaaS NestJS meetup Online #1
⽬次 \ お話したい / 1 1. はじめに 2. NestJS ×
マルチテナント × 認証 3. NestJS × マルチテナント × MongoDB 4. NestJS × マルチテナント × ロギング 5. おわりに
\ あなたと⼀緒に働きたい! / はじめに
\ 積極採⽤中 / ⾃⼰紹介 3
\ 全職種採⽤中 / 4
\ 積極採⽤中 / 会社紹介 ⽇本初の商品を連発している保険会社です。 5
\ 積極採⽤中 / 会社紹介 そのノウハウを元に保険SaaSを提供しています。 6 顧客 保険会社* *事業会社や保険代理店 のご利用も可能
プラン選択 本人認証 告知・重要事項説明 会員資格確認 商品ページ(LP) & 申込フォーム 契約参照 異動・解約 決済 契約更新 お客様 ポータル 査定・承認 問合せ 提出書類の参照 (電子データ) 支払記録 保険金 請求フォーム
\ 積極採⽤中 / 会社紹介 保険業務をSaaSでなめらかにし、みなさんがよい保険にアクセスしやすいようにしています。 7
\ あなたと⼀緒に働きたい! / サンプルコード
\ 全職種採⽤中 / 9
\ あなたと⼀緒に働きたい! / NestJS × マルチテナント × 認証
\ 積極採⽤中 / TL;DR • 認可トークンを⽤いたテナントIDの取得を⼀箇所で⾏うため、AuthGuardを⽤いる • テナントIDをLoggerに注⼊するために、useClass構⽂を⽤いる 11
\ 積極採⽤中 / AuthGuardを⽤いる • ヘッダーやパス、サブドメインからテナントIDを取得する場合、必ずしもAuthGuardは必要ではない • OAuthを⽤いて認可を⾏い、認可トークンからテナントIDを取得する場合、AuthGuardは必要 • 今回のデモでは簡略化のためヘッダーからテナントIDを取得する
12 @Injectable() export class AuthGuard implements CanActivate { constructor(private readonly logger: PinoLogger) { } canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> { const request: Request = context.switchToHttp().getRequest(); … const tenantId = request.headers['x-tenant-id’]; if (tenantId !== undefined) { request['tenantId'] = tenantId; this.logger.debug(`canActivate(): tenantId=${tenantId}`); this.logger.assign({ tenantId }) return true } } }
\ 積極採⽤中 / useClassを⽤いる • NestJSでは、GuardのようなMiddleWareもProviderである • ただし、`app.useGlobalGuards()` で追加した場合、DIのタイミングを逃してしまう •
AppModuleのようなトップレベルのモジュールに対し、特定のInjectionTokenを⽤いてInjectすることで、DIのタイミングを 逃さずにGlobalGuard同様に運⽤できる 13
\ 積極採⽤中 / useClassを⽤いる DIのタイミングを逃す例 14 app.useGlobalGuards(new AuthGuard()); @Module({ imports:
[…], controllers: […], providers: [ AppService, { provide: APP_GUARD, useClass: AuthGuard } ], }) export class AppModule { } AppModuleに対して明⽰的にInjectしている例
\ 積極採⽤中 / デモ 15
\ 積極採⽤中 / まとめ • AuthGuardで認証を⾏い、テナントIDをどこからでも取得可能にした • useClass構⽂でトップレベルのモジュールにAuthGuardを注⼊することで、LoggerをDIできた 16
\ あなたと⼀緒に働きたい! / NestJS × マルチテナント × MongoDB
\ 積極採⽤中 / TL;DR • MongoDBのDatabaseでテナントを分割した • ORMにMongooseを選定した • MongooseのコネクションはDatabaseと1:1
• リクエストスコープでMongooseをInjectするとメモリ不⾜になる • Serviceのメソッド実⾏時、適切なコネクションでModelを⽣成する 18
\ 積極採⽤中 / MongoDBのDatabaseでテナントを分割した 前提 • AWS DocumentDBを⽤いる • テナントごとにデータを分離する必要がある
19 単位 Pros Cons Cluster セキュリティが最も⾼い インフラ・管理コストのいずれも⾼い Database インフラ費⽤がテナント数に⽐例しない 。RBACを活⽤しやすい DatabaseをまたいだJOINはできあいので、 マスターデータとテナント固有データを合 わせて使うには⼯夫が必要 Collection インフラ費⽤がテナント数に⽐例しない 。コネクションを使い回せるので、パフ ォーマンスが⾼い 特定のテナントのみアクセス可能なRoleを アクセスするのが⾯倒 Row インフラ費⽤がテナント数に⽐例しない 。実装は簡単 MongoDBはRLSをサポートしていない
\ 積極採⽤中 / ORMにMongooseを選定した MongoDB事例の多さから、⼿堅くMongooseを選定しました。 20 Package NestJS Support Pros
Cons mongoose 公式Module 実績あり。NestJS公式ドキュメン トに記載あり。トランザクション 使える。 複数Connectionをサポートしてい ない。 Typegoose 公式サポートなし クラスやデコレーターを使って Modelを素早く構築できる 設定難易度が⾼い TypeORM 公式Moduleあり 実績あり(ただしRDBの割合⾼) MongoDB4系をサポートしていな いため、トランザクションが使え ない MikroORM MikroORM公式の NestJS Module 事例が少ない Prisma 公式ドキュメント 解説のみ MongoDBサポートがPreview MongoDB SDK 柔軟性は⾼い ORM相当の処理を⾃分で書くなら
\ 積極採⽤中 / リクエストスコープでMongooseをInjectするとメモリ不⾜になる • 複数のDatabaseにテナントごとに接続するには、複数のConnectionをリクエストに応じて使い分ける必要がある • 公式のMongooseModuleは複数Connectionの保持に対応していない • 最も簡単なやり⽅はMongooseModuleをリクエストスコープで⽣成することだが…
21 https://stackoverflow.com/questions/55571382/how-to-change-a-database-connection-dynamically-with-request-scope-providers-in その後、必要なものをリクエストにアタッチし、リクエストごとにデータベースを変更できます。 したがって、Scope.REQUESTを使⽤します。注⼊スコープの詳細については、ドキュメントを参照してください。 以前に作成した同じ接続を使⽤するにはどうすればよいですか。
\ 積極採⽤中 / デモ 22
\ 積極採⽤中 / Serviceのメソッド実⾏時、適切なコネクションでModelを⽣成する やりかたは2通り考えられる。 1. DIを使わない。ConnectionのPoolを⾃前で持ち、サービスの呼び出し時にModelを⽣成する。 2. DIを使う。Modelをリクエストスコープで宣⾔し、ConnectionのPoolをするProviderをInjectする。 23
\ 積極採⽤中 / DIを使わないサンプル 24 @Injectable() export const CatsService {
constructor() { private readonly connectionProvider: ConnectionProvider; } async getCats() { const connection = await this.connectionProvider.getConnection(); const cats = await connection.model('cats').find(); return cats; } } @Injectable() export class ConnectionProvider{ // 省略 getConnection() { const tenant = this.request.params.tenantId; } }
\ 積極採⽤中 / DIを使うサンプル 25 export const DogSchema = SchemaFactory.createForClass(Dog);
export const DogModelInjectionToken = "DogModel" export const dogModelFactory = { provide: DogModelInjectionToken, useFactory: (mongoConnectionMapProvider: MongoConnectionMapProvider, request: Request & { tenantId: string }) => { const tenantId = request.headers['x-tenant-id'] as string console.debug(`dogModelFactory.useFactory(): tenantId=${tenantId}`) return mongoConnectionMapProvider.getConnection(tenantId).model("Dog", DogSchema) }, inject: [MongoConnectionMapProvider, REQUEST] }
\ 積極採⽤中 / デモ 26
\ 積極採⽤中 / まとめ 単にMongooseModuleをリクエストスコープで利⽤するとコネクション数に問題が発⽣する。 MongoDBのコネクションを⾃前で管理し、Model⽣成時に適切に注⼊することで要件とパフォーマンスを両⽴できる。 27
\ あなたと⼀緒に働きたい! / NestJS × マルチテナント × ロギング
\ 積極採⽤中 / TL;DR • ログにリクエストIDとテナントIDを含める • 全てのErrorをCatchするExceptionsFilterを実装し、エラーを確実にログする 29
\ 積極採⽤中 / ログにリクエストIDとテナントIDを含める。 • ログへのリクエスト情報付与のために、nestjs-pinoを⽤いる • AuthGuardでリクエストIDを取得する際に、loggerにテナントIDをassignすることで、そのリクエストに対するログにテ ナントIDを付与できる(厳密なスコープは未検証) 30
canActivate(context: ExecutionContext,): boolean | Promise<boolean> | Observable<boolean> { const request: Request = context.switchToHttp().getRequest(); const tenantId = request.headers['x-tenant-id’]; if (tenantId !== undefined) { request['tenantId'] = tenantId; this.logger.debug(`canActivate(): tenantId=${tenantId}`); this.logger.assign({ tenantId }) return true } }
\ 積極採⽤中 / デモ 31
\ 積極採⽤中 / 全てのErrorをCatchするExceptionsFilterを実装し、エラーを確実にログする • NestJSは、デフォルトでは全てのエラーをロギングするわけではない • ドキュメントの通り、全てのエラーをキャッチするExceptionsFilterを実装する • useClass構⽂を⽤いてExceptionsFilterを注⼊することで、AuthGuardで設定したPinoLoggerを利⽤できる。
32 @Catch() export class AllExceptionsFilter implements ExceptionFilter { constructor( private readonly httpAdapterHost: HttpAdapterHost, private readonly logger: PinoLogger, ) { } …
\ 積極採⽤中 / デモ 33
\ 積極採⽤中 / まとめ • nestjs-pinoでログを整形し、かつリクエストIDとテナントIDを含める • ⾃前で実装したExceptionsFilterをuseClass構⽂を⽤いて注⼊することで、全てのエラーをテナントID付きでログに出⼒で きる 34
\ あなたと⼀緒に働きたい! / おわりに
\ 積極採⽤中 / チームについて • 今回ご紹介したのは、justInCaseTechnologiesでの取り組みの⼀部です • 私だけでなく、チームメンバーと⼀緒に取り組んだ成果でもあります • もっと知りたいという⽅、ぜひお話したいです!
36
\ 積極採⽤中 / チームの技術スタック バックエンド • NestJS, Fastify • pnpm
• TypeScript フロントエンド • React, Redux, Next.js • MUI, Emotion, Storybook • Jest, SWC • TypeScript インフラ • AWS, ECS, DocumentDB, Backup, ALB, WAF, GuardDuty, CodePipeline, Control Tower, AWS SSO, CDK v2, TypeScript • Vercel • GitHub Actions 37
\ 積極採⽤中 / チームの⽂化 • 物理的なホワイトボードの良さも認めるが、全員リモートワークが⼤好き • フルスタックかつ専⾨領域がある、T字型スキル志向の⽅が多い • いらないMTGを消したときに達成感を覚える
• Slackのハドルで突発的にペアプロが始まる • 議論の前に、NotionでPros/Consを整理しておく • 同じくらいのスキルの⼈がいたら、よりチームにないバックグラウンドの⼈にオファーする • いい仕事をした時はお互いに褒め合う • スクラム開発を尊ぶ 38
\ 全職種採⽤中 / 39
\ あなたと⼀緒に働きたい! / Thank you for listening!!!