個人開発プロジェクトのバックエンドをNestJS
で作成した。 公開したいので、aws
のLambda
に乗せて動かしてみることにした。
初めての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.json
のesModuleInterop
を有効にする。
{
"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.yml
のservice: 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-example
をservice: 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_modules
にdevDependencies
も含まれているからなので、Dependencies
のみのパッケージをインストールするnpm install --production
コマンドでnode_modules
を作成する。
ついでにzip
ファイルにpackage.json
とpackage.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}
ただ、どうも接続できないエンドポイントがある。
調べてみるとNestJS
のsynchronize
オプションをTrue
にしたままだったのが原因っぽい。
次は踏み台サーバーを作成し、RDS
の中身を見てみる。