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
1k
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
マルチモーダル理解と生成の統合 DeepSeek Janus, etc... / Multimodal Understanding and Generation Integration
hiroga
0
400
LlamaGen: LlamaのNext-Token予測を使った画像生成 / Autoregressive Model Beats Diffusion: Llama for Scalable Image Generation
hiroga
0
340
人事評価GPTsで評価の本質に向き合おう! / HR GPTs: Essential evaluations focus!
hiroga
1
400
生成AI元年を個人的に振り返る / Reflecting on First Year of the Generative-AI
hiroga
0
340
AWS Startup Day 2023 今日ここで! コスト削減ハンズオン / Cost-Saving Hands-On today!
hiroga
0
19
ChatGPT社内活用資料 / Internal use of ChatGPT
hiroga
0
98
マルチテナントSaaSのカスタム要件に、 Auth0テナントを分割せず向き合う! / Multi tenant SaaS with Auth0
hiroga
1
2.8k
雑な攻撃からELBを守る一工夫 +おまけ / Know-how to protect servers from miscellaneous attacks
hiroga
0
2.5k
[AWS CDK] 1,000+のCloudWatch Alarmsを自動生成する技術 / [AWS CDK] Technics to Generate 1,000+ CloudWatch Alarms
hiroga
2
1.6k
Other Decks in Technology
See All in Technology
ユーザーストーリーマッピングから始めるアジャイルチームと並走するQA / Starting QA with User Story Mapping
katawara
0
260
人はなぜISUCONに夢中になるのか
kakehashi
PRO
6
1.7k
Iceberg Meetup Japan #1 : Iceberg and Databricks
databricksjapan
0
130
Autonomous Database Serverless 技術詳細 / adb-s_technical_detail_jp
oracle4engineer
PRO
17
45k
明日からできる!技術的負債の返済を加速するための実践ガイド~『ホットペッパービューティー』の事例をもとに~
recruitengineers
PRO
3
500
転生CISOサバイバル・ガイド / CISO Career Transition Survival Guide
kanny
3
1.1k
運用しているアプリケーションのDBのリプレイスをやってみた
miura55
1
830
ビジネスモデリング道場 目的と背景
masuda220
PRO
9
670
抽象化をするということ - 具体と抽象の往復を身につける / Abstraction and concretization
soudai
27
14k
生成 AI プロダクトを育てる技術 〜データ品質向上による継続的な価値創出の実践〜
icoxfog417
PRO
5
1.8k
一度 Expo の採用を断念したけど、 再度 Expo の導入を検討している話
ichiki1023
1
230
エンジニアの育成を支える爆速フィードバック文化
sansantech
PRO
3
1.1k
Featured
See All Featured
Building a Modern Day E-commerce SEO Strategy
aleyda
38
7.1k
Done Done
chrislema
182
16k
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
27
1.9k
Reflections from 52 weeks, 52 projects
jeffersonlam
348
20k
A Modern Web Designer's Workflow
chriscoyier
693
190k
RailsConf 2023
tenderlove
29
1k
How GitHub (no longer) Works
holman
314
140k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
29
2.4k
What’s in a name? Adding method to the madness
productmarketing
PRO
22
3.3k
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
49
2.3k
Measuring & Analyzing Core Web Vitals
bluesmoon
6
250
Statistics for Hackers
jakevdp
797
220k
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!!!