NestJS をシンプルに初期構築から試す（その２ API Gateway + AWS Lambdaとしてデプロイ）

このページは、その２（その１のつづき）です。
- [その１](https://qiita.com/ssugimoto/items/210c21ec18b3c5c3383f)は、NestJSのWebフレームワークとしてローカル環境として動かす初期構築まで
- その２では、Amazon API Gateway + AWS Lambdaへデプロイを行います
- [その３では、AWS App Runner として動かす（仮）](https://qiita.com/ssugimoto/items/3db0616186ac545339e5)

|記事 |ブランチ |内容|
|---|---|---|
|[その１](https://github.com/ssugimoto/serverless-framework-nestjs-example/tree/feature/example-step1)|feature/example-step1 |nest cli |
|[その２](https://github.com/ssugimoto/serverless-framework-nestjs-example/tree/feature/example-step2-sls)|feature/example-step2-sls |use serverless framework, sls deploy|

将来、１つのdevelop ブランチ にしてしまうかもしれません



## Serverless Framework(sls)とAWS Lambda関連の対応

### 1. パッケージインストール
- 公式のドキュメント
    - https://docs.nestjs.com/faq/serverless
    - https://docs.nestjs.com/faq/serverless#example-integration
- 公式のexample
    - https://github.com/serverless/examples/blob/v3/aws-node-typescript-nest/serverless.yml

をもとに
追加でパッケージインストール

```
$ npm i @vendia/serverless-express aws-lambda
$ npm i -D @types/aws-lambda serverless-offline
```

### 2. ファイル`serverless.yml`を作成

```serverless-nest/serverless.yml
service: serverless-example
frameworkVersion: "3"

plugins:
  - serverless-offline

provider:
  name: aws
  runtime: nodejs18.x

functions:
  main:
    # handler: src/main.handler , srcかdistか、、、AWSでのLambdaの設定に関わる
    handler: dist/main.handler
    events:
      - http:
          method: ANY
          path: /
      - http:
          method: ANY
          path: '{proxy+}'


```
上記ファイルは一部省略しているため、GitHubリポジトリの情報を参照してください（ブランチ https://github.com/ssugimoto/serverless-framework-nestjs-example/blob/feature/example-step2-sls/serverless.yml ）

### 3. src/main.ts を AWS Lambdaのhandler向けに変更
- AWS Lambdaの作法に合わせる
```src/main.ts
import { NestFactory } from '@nestjs/core';
import serverlessExpress from '@vendia/serverless-express';
import { Callback, Context, Handler } from 'aws-lambda';
import { AppModule } from './app.module';

let server: Handler;

async function bootstrap(): Promise<Handler> {
  const app = await NestFactory.create(AppModule);
  await app.init();

  const expressApp = app.getHttpAdapter().getInstance();
  return serverlessExpress({ app: expressApp });
}

export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback,
) => {
  server = server ?? (await bootstrap());
  return server(event, context, callback);
};

```

- 変更前の`src/main.ts`を記録として残しておいた
```src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();
```

### 4. 上記では、まだ足らないのでServerless frameworkのpluginを導入
- https://www.serverless.com/plugins/serverless-plugin-typescript

引用
```
Usage with serverless-offline
The plugin integrates very well with serverless-offline to simulate AWS Lambda and AWS API Gateway locally.

Add the plugins to your serverless.yml file and make sure that serverless-plugin-typescript precedes serverless-offline as the order is important:
```

```
serverless.ymlファイルにプラグインを追加し、
順番が重要なのでserverless-plugin-typescriptがserverless-offlineより前にあることを確認する
```

- plugin の install
```
npm install -D serverless-plugin-typescript typescript
npm install -D serverless-plugin-optimize
```

- 実行するとserverless-offline も合わせてdevDependenciesにインストールされる、typescriptはマイナーバージョンアップ
```
"devDependencies": {
    "@nestjs/cli": "^10.0.0",
    "@nestjs/schematics": "^10.0.0",
    "@nestjs/testing": "^10.0.0",
    "@types/aws-lambda": "^8.10.119",
    "@types/express": "^4.17.17",
    "@types/jest": "^29.5.2",
    "@types/node": "^20.3.1",
    "@types/supertest": "^2.0.12",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint": "^8.42.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-prettier": "^5.0.0",
    "jest": "^29.5.0",
    "prettier": "^3.0.0",
    "serverless-offline": "^12.0.4",
    "serverless-plugin-typescript": "^2.1.5",
    "source-map-support": "^0.5.21",
    "supertest": "^6.3.3",
    "ts-jest": "^29.1.0",
    "ts-loader": "^9.4.3",
    "ts-node": "^10.9.1",
    "tsconfig-paths": "^4.2.0",
    "typescript": "^5.1.6"
  },
```

ここまでで、AWSへのデプロイ前までがほぼ完了しています
ここからは、awsへのデプロイとserverless frameworkを使いデプロイします

### 5. aws profile
- Serverless Frameworkを使い AWSにデプロイする上で、今回は `aws profile` を使います
- CI/CDとして使う場合では、別の方法になると思います
- 参考
    - AWS Credentials https://www.serverless.com/framework/docs/providers/aws/guide/credentials

#### aws profile 設定の参考です
リージョンはバージニアリージョンにしていますが、`serverless.yml`の `provider` > `region` が使われます。

```
$ cat ~/.aws/config 
[default]
region=us-east-1

$ cat ~/.aws/credentials 
[default]
aws_access_key_id=AK....HE
aws_secret_access_key=....
```

### 6. sls ビルド準備
- serverless.yml の custom `serverlessPluginTypescript`でtsconfig.jsonとは別のファイルに変更できたりします
- ファイル `./tsconfig.json` を使う場合は`serverlessPluginTypescript`の設定は不要です

```serverless.yml
service: serverless-example

frameworkVersion: "3"

plugins:
  - serverless-plugin-typescript
  - serverless-plugin-optimize
  - serverless-offline

custom:
  serverless-offline:
    httpPort: 3000

  serverlessPluginTypescript:
    # tsConfigFileLocation: './tsconfig.lambda.json'
    tsConfigFileLocation: './tsconfig.build.json'
    #tsConfigFileLocation: './tsconfig.json'
  defaultStage: dev
...

```

- 参考
https://www.serverless.com/plugins/serverless-plugin-typescript


### 7.sls package

- パッケージングを実施するコマンド（デプロイまではしない）
- ビルド等の動作確認として使えるコマンドです
- sls deployの前にパッケージングが正しくできるか確認する時に使えます

```
$ sls package
Running "serverless" from node_modules

Packaging serverless-example for stage dev (us-east-1)
Compiling with Typescript...
Using local tsconfig.json - ./tsconfig.build.json
Typescript compiled.

✔ Service packaged (25s)

1 deprecation found: run 'serverless doctor' for more details
```

`sls package`が成功するとファイル `.serverless/main.zip` にパッケージングされたファイルが作成されます
- main.zipのファイルの中身の一例
```
main
├── dist
│   ├── app.controller.d.ts
│   ├── app.controller.js
│   ├── app.controller.js.map
│   ├── app.module.d.ts
│   ├── app.module.js
│   ├── app.module.js.map
│   ├── app.service.d.ts
│   ├── app.service.js
│   ├── app.service.js.map
│   ├── main.d.ts
│   ├── main.js
│   └── main.js.map
├── node_modules
│   ├── accepts
│   ├── acorn-node
│   ├── align-text

...

│   ├── @nestjs
│   ├── node-fetch
│   ├── @nuxtjs
│   ├── object-assign
...

│   ├── @vendia
│   └── xtend
└── package.json

```

- 無駄なファイルがたくさん、 node_modules もってきてしまっているため、後々の改善ポイントにしておきます

### 8. sls deploy
- AWSクラウドへのデプロイを行います
- sls packageが内部で先に実行されます
- Cloudformation＋S3が使われます

#### ビルドしてデプロイ
```
$ nest build && sls deploy
Running "serverless" from node_modules

Deploying serverless-example to stage dev (us-east-1)
Compiling with Typescript...
Using local tsconfig.json - ./tsconfig.build.json
Typescript compiled.

✔ Service deployed to stack serverless-example-dev (88s)

endpoints:
  ANY - https://xxx.execute-api.us-east-1.amazonaws.com/dev/
  ANY - https://xxx.execute-api.us-east-1.amazonaws.com/dev/{proxy+}
functions:
  main: serverless-example-dev-main (25 MB)

Monitor all your API routes with Serverless Console: run "serverless --console"
```
- API Gatewayのエンドポイントが生成されます（存在しなければ新規作成、存在していれば更新）
- API Gatewayのエンドポイントは１つです
- トリガー元がAPI GatewayのAWS Lambdaが生成されます（存在しなければ新規作成、存在していれば更新）
- Lambda Functionは複数ではなく、NestJSでのルーティングが使われるためデプロイされるLambdaは１つです
    - Lambda内で対応したメソッドと関数が呼び出されます

##### Cloudformationの結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482752/eebc5d29-24ad-9b99-9c95-1794d84adfce.png)

#### APIの動作確認
```
curl https://xxx.execute-api.us-east-1.amazonaws.com/dev/
```
```
curl https://xxx.execute-api.us-east-1.amazonaws.com/dev/say
```

#### sls deploy オプション
- `--stage` : サービスのステージ
- `--force` : 強制デプロイ


---

ここまでで、AWSクラウド側に REST APIを作成できました。認証や認可の対応等を機能追加していくとよくあるWebアプリケーションに近づいていきます。

---

## Serverless Frameworkの知らなかった機能

###  `sls print` で Variables、パラメーターを確認できる

```serverless.yml

service: serverless-example

..略

custom:
  serverless-offline:
    httpPort: 3000
    stageVariables:
      foo: "bar"
  defaultStage: dev
  environment:
    dev: ${file(./env/site-dev.yml)}
    prod: ${file(./env/site-prod.yml)}

provider:
  name: aws
  runtime: nodejs18.x
  region: us-east-1
  stage: ${opt:stage, self:custom.defaultStage}
```

のような場合に

```
$ sls print --stage prod
```
を実行すると、以下のように確認できる

```
Running "serverless" from node_modules
service: serverless-example

...略

custom:
  serverless-offline:
    httpPort: 3000
    stageVariables:
      foo: bar
  defaultStage: dev
  environment:
    dev:
      key: sample
    prod:
      key: prod-sample
provider:
  name: aws
  runtime: nodejs18.x
  region: us-east-1
  stage: prod
  versionFunctions: true

```

## serverless-offlineの意図せぬ挙動
### ドメインRootへのアクセスの挙動（plugin `serverless-offline` の挙動）
- ドメインルートにアクセスすると動作しない、sls offline の問題
- ルートアクセス用の `getHello()` に到達しない
- src/app.controller.ts
```app.controller.ts

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    console.log('controller start.');
    return this.appService.getHello();
  }

  @Get('say')
  getSay(): string {
    return 'say.';
  }
}
```

#### curl でドメインルートアクセスした場合

挙動その１
```
>curl http://localhost:3000/
{"currentRoute":"get - /","error":"Serverless-offline: route not found.","existingRoutes":["* - /dev","* - /dev/{proxy*}"],"statusCode":404}
```

挙動その２
```
>curl http://localhost:3000/dev/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET null</pre>
</body>
</html>

```

#### curl でドメインルートに、スラッシュを１つ多くしてアクセスすると `getHello()` に到達する

```
>curl http://localhost:3000/dev//
Hello World!
```

- curl で API Gatewayのドメインルートにアクセスした場合

```
>curl https://xxx.execute-api.us-east-1.amazonaws.com/dev/
Hello World!

>curl https://xxx.execute-api.us-east-1.amazonaws.com/dev//
Hello World!

>curl https://xxx.execute-api.us-east-1.amazonaws.com/dev/say
say.
```

### 対処というか回避
- https://stackoverflow.com/questions/57733581/serverless-offline-route-not-found-running-an-aws-lambda-function-in-offline
- https://qiita.com/manasan-iTL/items/e38bc39ab90d1191b6e1
- https://github.com/dherault/serverless-offline/issues/911
- https://github.com/dherault/serverless-offline/issues/88
- 放置
    - API Gateway + AWS LAmbda でのデプロイ時は上に記述の通り動くので放置しておく


## 余談、NestJSをAWSクラウドで動かす場合
- EC2 で 動かす（dockerコンテナ使っても良いですし、プロセスとしてミドルウェアとしても動かせる）
- ALB + ECS Fargate で `app.listen(3000);` のようにWebサーバー相当でリクエストを受ける
- ✅APIとして API Gateway + AWS Lambdaとして動かす
- 他には 
    - API Gateway + Fargate 
    - App Runner もあります
- AWSだと、Codeシリーズを使いCI/CDできます