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
1.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
590
LlamaGen: LlamaのNext-Token予測を使った画像生成 / Autoregressive Model Beats Diffusion: Llama for Scalable Image Generation
hiroga
0
470
人事評価GPTsで評価の本質に向き合おう! / HR GPTs: Essential evaluations focus!
hiroga
1
430
生成AI元年を個人的に振り返る / Reflecting on First Year of the Generative-AI
hiroga
0
370
AWS Startup Day 2023 今日ここで! コスト削減ハンズオン / Cost-Saving Hands-On today!
hiroga
0
140
ChatGPT社内活用資料 / Internal use of ChatGPT
hiroga
0
130
マルチテナントSaaSのカスタム要件に、 Auth0テナントを分割せず向き合う! / Multi tenant SaaS with Auth0
hiroga
1
3k
雑な攻撃からELBを守る一工夫 +おまけ / Know-how to protect servers from miscellaneous attacks
hiroga
0
2.6k
[AWS CDK] 1,000+のCloudWatch Alarmsを自動生成する技術 / [AWS CDK] Technics to Generate 1,000+ CloudWatch Alarms
hiroga
2
1.7k
Other Decks in Technology
See All in Technology
PdM業務における使い分け
shinshiro
0
590
自分がLinc’wellで提供しているプロダクトを理解するためにやったこと
murabayashi
1
160
AI エンジニアの立場からみた、AI コーディング時代の開発の品質向上の取り組みと妄想
soh9834
7
410
MCPと認可まわりの話 / mcp_and_authorization
convto
2
180
データエンジニアリング 4年前と変わったこと、 4年前と変わらないこと
tanakarian
2
370
(HackFes)米国国防総省のDevSecOpsライフサイクルをAWSのセキュリティサービスとOSSで実現
syoshie
5
660
Expertise as a Service via MCP
yodakeisuke
1
150
AWS Well-Architected から考えるオブザーバビリティの勘所 / Considering the Essentials of Observability from AWS Well-Architected
sms_tech
1
860
「AI駆動開発」のボトルネック『言語化』を効率化するには
taniiicom
1
160
From Live Coding to Vibe Coding with Firebase Studio
firebasethailand
1
180
経理出身PdMがAIプロダクト開発を_ハンズオンで学んだ話.pdf
shunsukenarita
1
150
MCPに潜むセキュリティリスクを考えてみる
milix_m
1
760
Featured
See All Featured
Navigating Team Friction
lara
187
15k
Building a Scalable Design System with Sketch
lauravandoore
462
33k
Building Better People: How to give real-time feedback that sticks.
wjessup
367
19k
Rails Girls Zürich Keynote
gr2m
95
14k
Done Done
chrislema
184
16k
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
109
19k
Git: the NoSQL Database
bkeepers
PRO
431
65k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
31
1.3k
The World Runs on Bad Software
bkeepers
PRO
70
11k
Visualization
eitanlees
146
16k
The Pragmatic Product Professional
lauravandoore
35
6.8k
Art, The Web, and Tiny UX
lynnandtonic
301
21k
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!!!