個人開発プロジェクトのバックエンドをNestJSで作成した。 公開したいので、awsLambdaに乗せて動かしてみることにした。

初めてのaws、いつかの自分のために記録として一挙手一投足のログを残しておくことにする。

前提

$ node -v
v18.16.0

"@nestjs/cli": "^9.0.0",

バージョンは上記の通り。

で、Dockerの設定ファイルを諸々作ったが、使っていないので、ない前提でいく。

さて、それでは進めていく。

AWSアカウントの作成

AWSの公式ウェブサイトでアカウントを作成する必要がある。

ルートユーザーの E メールアドレス
AWS アカウント名

で新規アカウント作成ができる。

その後、認証コードを経て、

パスワード
連絡先情報(英語で書けって言われたのでchatGPTに住所教えて英語にしてもらった...個人情報)
請求情報
本人確認
サポートプランを選択→無料のベーシックを選択

AWS アカウント作成の流れを参考にすればいけるけど、意外と時間画かかったし、アカウント作成時にクレジットカードを登録することはいい気がしない。

とりあえず、これでアカウント作成が完了した。

必要なパッケージのインストール

ここからはNestJS-サーバーレスの公式を参考にしていく。

説明のために、Nest (@nestjs/platform-express完全に機能する HTTP ルーター全体を使用して起動) をサーバーレスフレームワーク (この場合は AWS Lambda をターゲットとしています) と統合します。

公式もawsで説明してくれているので、ありがたい。

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

まずは上記の通り、必要なパッケージをインストールする。

    "@vendia/serverless-express": "^4.10.4",
    "aws-lambda": "^1.0.7",

    "@types/aws-lambda": "^8.10.119",
    "serverless-offline": "^12.0.4",

上記のようなバージョンがインストールされた。

次に、serverless.ymlファイルを作成する。

service: serverless-example

plugins:
  - serverless-offline

provider:
  name: aws
  runtime: nodejs14.x

functions:
  main:
    handler: dist/main.handler
    events:
      - http:
          method: ANY
          path: /
      - http:
          method: ANY
          path: '{proxy+}'

とりあえず公式をコピペしたやつ。

service: serverless-example
↓
service: backend

という風にpackage.jsonに合わせた。

node.jsのバージョンも揃えたほうがいいのかもだけど、いったんこのままで進める。

次にmain.tsを編集する。

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './shared/filter/http-exception.filter';
import serverlessExpress from '@vendia/serverless-express';
import { Callback, Context, Handler } from 'aws-lambda';

let server: Handler;


async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.init();

  // Swaggerの記述とかバリデーションの記述

  (...略)

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


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

のように変更した。

最後にtsconfig.jsonesModuleInteropを有効にする。

{
  "compilerOptions": {
    ...
    "esModuleInterop": true
  }
}

で、下記コマンドを打つと

 npm run build

アプリケーションが実行されたら、ブラウザを開いてhttp://localhost:3000/dev/ANY_ROUTE に移動します。

ということなので、やってみる。

が、「このサイトにアクセスできません」になる。

そもそもサーバーを立ち上げてないので動くわけがない。

じゃあどうするのか。

いらいらググると、

$ npx serverless offline

というコマンドを叩く必要がありそう。てかドキュメントにも書いてあった。

そういうことなので実行してみる。

Need to install the following packages:
  serverless@3.32.2
Ok to proceed? (y)

なんか出たけど、y

入っていないパッケージを参照するよ、ということらしい。

Server ready: http://localhost:3000 🚀


ANY /dev/api/v1/users/name (λ: main)
× [504] - Lambda timeout.


ANY /dev (λ: main)
× [504] - Lambda timeout.


ANY /dev/api (λ: main)
× [504] - Lambda timeout.

なんかタイムアウトしてるけど、動いているっぽい。

タイムアウトの原因をデータベースとの接続と考え、データベースを介さない、ただ文字列を返却するだけのエンドポイントを作成し、テストする。

変わらず、タイムアウトした。

仕方ないので、main.tsをまるっとドキュメントのコピペにした。

これで動けばmain.tsに問題がある。

動かない。

次にserverless.ymlservice: serverless-exampleを戻したら、データベースを介さないやつは動いた。

そしてまた何回か繰り返すとタイムアウトした。

再度起動しなおしたら、データベースの値を取得出来た。

そういうわけで、main.tsをもとに戻してみる。

タイムアウトした。

main.tsの原因を探る。

import { NestFactory } from '@nestjs/core';
import serverlessExpress from '@vendia/serverless-express';
import { ValidationPipe } from '@nestjs/common';
import { Callback, Context, Handler } from 'aws-lambda';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { HttpExceptionFilter } from './shared/filter/http-exception.filter';

let server: Handler;


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


  // Swaggerの記述とかバリデーションの記述

  (...略)

  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);
};

これにしたら動いた。

bootstrap();

の実行場所が問題だったっぽい。

というわけでservice: serverless-exampleservice: backendに戻した。

これでサーバーレスのテストをローカルでできた。

追記:2023年6月30日

ローカルでテストする際に、timeoutが頻発するので調べたところ、

プロバイダーの下にタイムアウトを追加します。ラムダのタイムアウトの最大値は 900 秒です。実行時間に応じて 30 秒などに設定し、何が起こるかを確認してください。

という記事を見つけた。

実際に

provider:
  name: aws
  runtime: nodejs18.x
  timeout: 30

にしたところ、タイムアウトしなくなった。

AWSにデプロイ

次にawsにデプロイする必要があると思っている。

その為に何をすればいいのか調べて実行していく。

もしかして、コンソールに入り関数を作成し、zipファイルをアップロードすればいいだけ?

そんな気がするのでそれでやってみる。

Lambdaの関数作成

AWS Lambda ダッシュボードの 関数を作成をクリックする。

一から作成を選択し、関数名を決める。

次に、関数の記述に使用する言語を選択します。ということでNode.js 14.xを選択した。理由してはローカルでサーバレスのテストをした時のランタイムにNode.js 14.xを使用したので。

よく考えたら18で作ってるんだけど、今回は気にしないことにする。

(※このログを書いた後にNode.js 18.xにした。)

アーキテクチャっていうのは

x86_64
arm64

のふたつがあってよくわからないので、x86_64を選択した。

一応調べてみると下記に詳しい。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/foundation-arch.html

どうやら値段が違うようで、arm64の方が安そうな気がする。

ということで、arm64を選択することにした。

(※このログを書いた後にx86_64にした。)

で、関数の作成を終了する。

Zip のアップロード

作成後遷移したページでコードタブをクリックし、npm run buildで作成されたdistフォルダをzip化し、アップロードする。

API Gatewayの構築

次に API Gatewayのダッシュボードにいき、HTTP APIの構築を行う。

統合で先ほど作成した関数を選択する。API名は特に決まりないよう。

次にルート設定でリソースパスを/{proxy+}に変更する。

最後に「ステージを設定」は変更せず、そのまま 作成をする。

どうやら次に権限の設定があるらしい。

API Gateway コンソールで、先ほど作成した API の詳細ページを開く。左のタブから、「統合」を選択し、ルートの統合の詳細を見る。

アクセス許可を呼び出すのポリシーステートメント例の、source-arn内の文字列をコピーしておく。

またLambdaに戻り、設定→アクセス権限、リソースベースポリシー内の アクセス権限を追加をクリック。

AWSのサービス を選択し、下の画像のように入力して保存します。ソースARNに先ほどの文字列を追加。

アクションはlambda:InvokeFunctionを選択する。

URLにアクセスするが、{"message":"Internal Server Error"}になる。謎が深い。

serverless.ymlにならい、dist/main.handlerにハンドラを変更したが変わらない。

検索するとHTTP API Lambda 統合に関する問題のトラブルシューティングのページにたどり着いたので、見ていく。

最近気が付いたけど、結局公式見るのが一番早い。

急がば回れ、誰が考えたか知らないけど、先人の知恵ほど偉大なものはない。

内部サーバーエラーのトラブルシューティングを行うには、ログ形式に $context.integrationErrorMessage ログ記録変数を追加し、HTTP API のログを表示します。これを達成するには、次の操作を行います。

という操作をするらしいけど、何言ってるのか全くわからない。

とりあえず、下記公式の通り進める。

CloudWatch コンソール (https://console.aws.amazon.com/cloudwatch/) を開きます。

[ロググループ] を選択します。

[ロググループの作成] を選択します。

ロググループ名を入力し、[作成] を選択します。

ロググループの Amazon リソースネーム (ARN) を書き留めます。ARN 形式は、arn:aws:logs:region: account-id:log-group:log-group-name です。HTTP API のアクセスのログ記録を有効にするには、ロググループ ARN が必要です。

やろうと思ったけど、既にグループがあったので、「ロググループの Amazon リソースネーム (ARN) を書き留めます。」だけやった。

次に下記の公式通りの手順を行う。

https://console.aws.amazon.com/apigateway で API Gateway コンソールにサインインします。

HTTP API を選択します。

[モニタリング] で、[ログ記録] を選択します。

API のステージを選択します。

[編集] を選択し、アクセスログを有効にします。

[Log destination] (ログの送信先) で、前のステップで作成したロググループの ARN を入力します。

[ログの形式] で、[CLF] を選択します。

ログ形式の末尾に $context.integrationErrorMessage を追加します。

[保存] を選択します。

Network Failureでできない(※ 今思えば、タイムアウトしていた?)。謎が深い。

諦めて、ロググループにあった既存のグループのログを見てみると、@nestjs/coreモジュールを見つけることができない的なことが書いてある。

それをchat GPTに聞くとnode_modulesもアップロードしろという返答が返ってきたので試してみる。

重すぎてあげられない。

s3を使えとのこと。

そういうことであればと思うのだが、お金発生しているのか不安になる。

調べるとAWS 無料利用枠があるっぽいので行ってみる。

アップロードしたs3のデータをLambdaにアップロードする。

リレージョンが異なるので無理っぽい ??

よくよく見ると、Lambdaのリレージョンシドニーでやってた。

東京に作り直す。

東京に作り直したけど、s3のファイルサイズが大きすぎると怒られた。

たぶん、node_modulesdevDependenciesも含まれているからなので、Dependencies のみのパッケージをインストールするnpm install --productionコマンドでnode_modulesを作成する。

ついでにzipファイルにpackage.jsonpackage.look.jsonも入れてみる(結果的に正しかった)。

でも無理で、色々試した結果、distフォルダがダメだということに気が付いた。

つまり、

  • package.json
  • node_modules
  • main.ts etc

という構成にしなければならないことに気が付いた。

かつ、bcryptが使用できないいっぽいのでbcryptjsを使用するこにした。

で、あげたら今度はタイムアウトになった。 すこしづつ進んでいるが、まだできない。

基本設定でタイムアウトを30秒にした。

次に秘密鍵のエラーが出た。 JWTの秘密鍵をenvで管理しているのだからそりゃそうだ。

環境変数を設定すると、ログからエラーが消えたが、{"message":"Service Unavailable"}という結果になる。

新しい。

ログを見るとUnable to connect to the databaseということなのでデータベースに接続てきていないらしい。

ローカルのMySQLに接続できないだろし、これを機にRDSの構築をすることにした。

RDSの作成

Amazon RDS コンソールの [データベースページ] を開いて、[データベースの作成] を選択します。
[標準作成] オプションを選択したままにし、[エンジンのオプション] で [MySQL] を選択します。
[テンプレート] で、[無料利用枠] を選択します。
[設定] で、[DB インスタンス識別子] に ****** を入力します。
以下を実行して、ユーザー名とパスワードを設定します。
[認証情報の設定] では、[マスターユーザー名] の設定を admin のままにします。
[マスターパスワード] には、データベースにアクセスするためのパスワードを入力して確認します。
次の操作を実行して、データベース名を指定します。
残りのデフォルトオプションはすべて選択したままにして、[追加設定] ペインまで下にスクロールします。
このペインを展開し、[最初のデータベース名] として ****** を入力します。
残りのデフォルトオプションはすべて選択したままにして、[データベースの作成] を選択します。

やり方は公式に詳しかったのでそのようにした。

同時にLambdaの環境変数を編集した。

DATABASE_HOST:*****.rds.amazonaws.com(RDSのエンドポイント)
DATABASE_NAME	******
DATABASE_PASSWORD	******
DATABASE_PORT	3306  ← よくわからないけどローカルで設定していた数字のまま
DATABASE_USER	******

でエンドポイントを叩いたが、

{"message":"Service Unavailable"}

うまくいかない。

色々と調べてみると、IMAロールVPCの設定が必要とのこと。

https://qiita.com/kobayashi_0226/items/d8f97c652873d80b1367

上記記事を見つつ、ぽちぽちやっているとできた。

{"status":"success","data":{"newComment":[]},"message":null}

ただ、どうも接続できないエンドポイントがある。

調べてみるとNestJSsynchronizeオプションをTrueにしたままだったのが原因っぽい。

次は踏み台サーバーを作成し、RDSの中身を見てみる。