概要
NestJSにてプロジェクト新規作成からserverlessでのデプロイまでを簡単にまとめる。
前提
下記の作業が完了していること。
AWSのアカウントを持っており、AWS CLIが導入されており、IAMアクセスキーがCLIに設定されていること。
方法
-
前提で記載したNestJSのプロジェクトルート(nest-testing)の1階層上で、下記を実行してserverlessのプロジェクト作成
serverless create --template aws-nodejs-typescript --path nest-testing-serverless -
serverlessのプロジェクトルートに移動
cd nest-testing-serverless -
下記を実行してパッケージインストール
npm install -
serverless.tsを下記の様に修正- 実施内容
-
service名をtask-api-serverlessに設定 -
runtimeをnodejs20.xに変更 -
regionをap-northeast-1に追加 -
iamにDynamoDB全操作権限を追加 -
pluginsにserverless-offlineを追加 -
functionsをhello削除してNestJS用のapiに変更(/{proxy+}で全リクエストをNestJSに流す) -
resourcesにDynamoDBテーブル定義を追加 -
custom.esbuildにplugins: './esbuild.plugins.js'を追加 -
custom.esbuildのexternalにNestJS関連モジュールを追加 -
custom.esbuildのtargetをnode20に変更
-
serverless.tsimport type { AWS } from '@serverless/typescript'; const serverlessConfiguration: AWS = { service: 'task-api-serverless', frameworkVersion: '3', plugins: ['serverless-esbuild', 'serverless-offline'], provider: { name: 'aws', runtime: 'nodejs20.x', region: 'ap-northeast-1', iam: { role: { statements: [ { Effect: 'Allow', Action: ['dynamodb:*'], Resource: '*', }, ], } }, apiGateway: { minimumCompressionSize: 1024, shouldStartNameWithService: true, }, environment: { AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', NODE_OPTIONS: '--enable-source-maps --stack-trace-limit=1000', }, }, // import the function via paths functions: { api: { handler: 'src/handler.handler', events: [ { http: { method: 'any', path: '/{proxy+}' } } ] } }, package: { individually: true }, resources: { Resources: { UsersTable: { Type: 'AWS::DynamoDB::Table', Properties: { TableName: 'Users', AttributeDefinitions: [ { AttributeName: 'id', AttributeType: 'S' }, ], KeySchema: [ { AttributeName: 'id', KeyType: 'HASH' }, ], BillingMode: 'PAY_PER_REQUEST', }, }, }, }, custom: { esbuild: { bundle: true, plugins: './esbuild.plugins.js', minify: false, sourcemap: true, exclude: ['aws-sdk'], external: [ '@nestjs/websockets/socket-module', '@nestjs/microservices/microservices-module', '@nestjs/microservices', 'class-transformer', 'class-validator', ], target: 'node20', define: { 'require.resolve': undefined }, platform: 'node', concurrency: 10, }, }, }; module.exports = serverlessConfiguration; - 実施内容
-
tsconfig.jsonを下記のように修正- 実施内容
-
experimentalDecorators: trueを追加(デコレータを有効化) -
emitDecoratorMetadata: trueを追加(DIに必要なメタデータを埋め込む)
-
tsconfig.json{ "extends": "./tsconfig.paths.json", "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true, "lib": ["ESNext"], "moduleResolution": "node", "noUnusedLocals": true, "noUnusedParameters": true, "removeComments": true, "sourceMap": true, "target": "ES2020", "outDir": "lib" }, "include": ["src/**/*.ts", "serverless.ts"], "exclude": [ "node_modules/**/*", ".serverless/**/*", ".webpack/**/*", "_warmup/**/*", ".vscode/**/*" ], "ts-node": { "require": ["tsconfig-paths/register"] } } - 実施内容
-
下記を実行して必要なパッケージをインストール
# NestJS本体 npm install @nestjs/core @nestjs/common @nestjs/platform-express reflect-metadata rxjs # Lambda対応 npm install @codegenie/serverless-express # DynamoDB npm install zod @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb # 型定義 npm install --save-dev @types/aws-lambda # serverless-offline(v3対応版) npm install --save-dev serverless-offline@13 # esbuildデコレータ対応 npm install --save-dev @anatine/esbuild-decorators # ULIDを使うためのパッケージ npm install ulidx -
プロジェクトルートに
esbuild.plugins.jsを作成し、下記のように記載esbuild.plugins.jsconst { esbuildDecorators } = require('@anatine/esbuild-decorators'); module.exports = [esbuildDecorators()]; -
src/handler.tsを作成し、下記の様に記載(lambdaのエントリーポイント)src/handler.tsimport 'reflect-metadata'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import serverlessExpress from '@codegenie/serverless-express'; import { Handler, Context, Callback } from 'aws-lambda'; let server: Handler; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.init(); const expressApp = app.getHttpAdapter().getInstance(); return serverlessExpress({ app: expressApp }); } export const handler: Handler = async (event, context: Context, callback: Callback) => { server = server ?? (await bootstrap()); return server(event, context, callback); }; -
serverless側リポジトリルートで下記を実行して「nest-testing」のプロジェクトから必要なファイル郡をコピーで持って来る。
cp -r ../nest-testing/src/users ./src/ cp -r ../nest-testing/src/pipes ./src/ cp ../nest-testing/src/dynamodb.client.ts ./src/ cp ../nest-testing/src/app.module.ts ./src/ cp ../nest-testing/src/app.controller.ts ./src/ cp ../nest-testing/src/app.controller.spec.ts ./src/ cp ../nest-testing/src/app.service.ts ./src/ cp ../nest-testing/src/create-table.ts ./src/ -
src/dynamodb.client.tsを下記の様に書き換えてDB接続先をDockerコンテナを指定src/dynamodb.client.tsimport { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; const client = new DynamoDBClient({ region: 'ap-northeast-1', endpoint: 'http://localhost:8000', credentials: { accessKeyId: 'dummy', secretAccessKey: 'dummy', }, }); export const dynamoDb = DynamoDBDocumentClient.from(client); -
プロジェクトルートに
docker-compose.ymlを作成して下記の様に記載(AWSが公式に提供しているDockerイメージを利用しているのでDockerfileは不要)docker-compose.ymlservices: dynamodb-local: image: amazon/dynamodb-local ports: - "8000:8000" volumes: - dynamodb-data:/home/dynamodblocal command: "-jar DynamoDBLocal.jar -sharedDb -dbPath /home/dynamodblocal" volumes: dynamodb-data: -
下記を実行してコンテナを起動
docker-compose up -d -
下記を実行してローカルでserverlessを起動(Dockerなどが動いている場合停止してから下記を実行)
npx serverless offline -
下記を実行してテーブルを作成(「テーブル作成成功」と表示されたら完了)
npx ts-node src/create-table.ts -
下記を実行してローカル環境での動作確認を実施
# ユーザー作成 curl -X POST http://localhost:3000/dev/users \ -H "Content-Type: application/json" \ -d '{"name": "永続化太郎", "email": "persist_taro@example.com"}' # 一覧取得 curl http://localhost:3000/dev/users # 1件取得(作成で返ってきたidを使う) curl http://localhost:3000/dev/users/{id} # 更新 curl -X PATCH http://localhost:3000/dev/users/{id} \ -H "Content-Type: application/json" \ -d '{"name": "永続化次郎"}' # 一覧取得(更新された内容が返ることを確認) curl http://localhost:3000/dev/users # 削除 curl -X DELETE http://localhost:3000/dev/users/{id} # 一覧取得(何も返らないことを確認) curl http://localhost:3000/dev/users # 値チェックエラー確認 nameが空文字(min(1)違反) curl -X POST http://localhost:3000/dev/users \ -H "Content-Type: application/json" \ -d '{"name": "", "email": "persist_taro@example.com"}' # 値チェックエラー確認 emailの形式が不正 curl -X POST http://localhost:3000/dev/users \ -H "Content-Type: application/json" \ -d '{"name": "永続化太郎", "email": "これはメールじゃない"}' # 値チェックエラー確認 必須フィールドが欠けている curl -X POST http://localhost:3000/dev/users \ -H "Content-Type: application/json" \ -d '{"name": "永続化太郎"}' -
src/dynamodb.client.tsを下記の様に書き換えてDB接続先をデプロイ後のものを指定src/dynamodb.client.tsimport { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; const client = new DynamoDBClient({ region: 'ap-northeast-1', }); export const dynamoDb = DynamoDBDocumentClient.from(client); -
下記を実行してAWSにデプロイ(完了まで数分かかる。ローカル環境は止めても起動したままでも問題ない。)
npx serverless deploy -
実行結果の「endpoint」の/dev/までをコピーして下記を置き換えて実行
# ユーザー作成 curl -X POST https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/users \ -H "Content-Type: application/json" \ -d '{"name": "永続化太郎", "email": "persist_taro@example.com"}' # 一覧取得 curl https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/users # 1件取得(作成で返ってきたidを使う) curl https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/users/{id} # 更新 curl -X PATCH https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/users/{id} \ -H "Content-Type: application/json" \ -d '{"name": "永続化次郎"}' # 一覧取得(更新された内容が返ることを確認) curl https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/users # 削除 curl -X DELETE https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/users/{id} # 一覧取得(何も返らないことを確認) curl https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/users # 値チェックエラー確認 nameが空文字(min(1)違反) curl -X POST https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/users \ -H "Content-Type: application/json" \ -d '{"name": "", "email": "persist_taro@example.com"}' # 値チェックエラー確認 emailの形式が不正 curl -X POST https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/users \ -H "Content-Type: application/json" \ -d '{"name": "永続化太郎", "email": "これはメールじゃない"}' # 値チェックエラー確認 必須フィールドが欠けている curl -X POST https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/users \ -H "Content-Type: application/json" \ -d '{"name": "永続化太郎"}' -
下記を実行してデプロイしたリソース郡を削除
npx serverless remove
本作業実施後のコードはこちら
付録
ローカルターミナルからログを見るコマンド
npx serverless logs -f api --stage dev
デプロイ後にスキーマを変更するために一旦テーブルを削除するコマンド
aws dynamodb delete-table --table-name テーブル名 --region ap-northeast-1
デプロイしたものをすべて消すコマンド
npx serverless remove
デプロイするコマンド
npx serverless deploy