ユニバーサルJavaScriptサーバーNitroをAWS Lambdaにデプロイする

| 7 min read
Author: noboru-kudo noboru-kudoの画像

2022-04-19にハイブリッドVueフレームワークのNuxt3がRCバージョンになりました。

Nuxt3はVue3やTypeScript、Vite対応等の様々な改良・機能追加がありますが、大きな変更の1つにNitroエンジンの搭載があります。

Nitro自体はNuxtに依存するものではなく、Node.js上で動作する軽量・高速JavaScriptサーバーです。Nuxt3ではユニバーサルモードのサーバー側ビルド・実行環境に利用されています。
Nitroの大きな特徴として、当初からサーバーレス環境を前提としてデザインされており、AWS LambdaNetlifyVercel等の各種サーバーレスプロバイダー上で動作可能です。
このようなマルチプロバイダー対応や移植性の高さから、NitroはユニバーサルJavaScriptサーバーというのが売りのようです。

今回はNuxt3ではなく、あえてNitroにフォーカスしてその機能を試してみました(Nuxt3は別の機会で...)。

Contents

Nitroをセットアップする

#

Nitroドキュメント上でZero-Configセットアップと言っているだけあって簡単です。
任意のディレクトリでnpmをセットアップします。

mkdir nitro-sample
cd nitro-sample
npm init -y

Nitroをインストールします(現時点で最新のv0.4.12)。ここではNitro本体に加えてTypeScriptもインストール、初期化しました。

npm install --save-dev nitropack typescript
npx tsc --init

tscコマンドで生成されたtsconfig.jsonを修正します。以下関連部分のみの抜粋です。

{
"compilerOptions": {
"extends": "./.nitro/types/tsconfig.json"
}
}

これで準備完了です。

NitroでREST APIを作成する

#

Nitroは実装も最小限です。REST APIはroutesディレクトリを作成し、その中にリクエストを処理するスクリプトファイルを配置するだけです。
今回は/fooでアクセス可能なGET/POSTメソッドを用意しました。以下追加したファイルです。

  • routes/foo.get.ts
export default eventHandler((event) => {
const { name } = useQuery(event)
return `GET: ${name}`
})
  • routes/foo.post.ts
export default eventHandler(async (event) => {
const { name } = await useBody<{ name: string }>(event)
return `POST: ${name}`
})

非常にシンプルですね。それぞれのファイルがAPIのRouteになります。
Nitroは内部的にはH3という軽量サーバーを利用して、リクエストを処理します。
このroutesディレクトリ内のスクリプトは、ファイル名やディレクトリ構成に応じてそのままH3のRouteとしてマッピングされます。明示的なルーティングの定義は不要です。
実装方法の詳細は、以下NitroドキュメントやH3のドキュメントを参照してください。

また、上記スクリプトファイルにはimport文がありませんが、掲載を省略している訳ではありません。
NitroではAuto Import機能が備わっており、よく利用するものはimportを記述する必要がありません。

これをローカル環境で起動します。以下のコマンドを実行します。

npx nitropack dev

デフォルトはローカルホストの3000ポートでサーバーが起動します。
以下curlでの動作確認です。

curl localhost:3000/foo?name=mamezou
> GET: mamezou

curl localhost:3000/foo -d '{"name": "mamezou"}' -H 'Content-Type: application/json'
> POST: mamezou

リクエストが正常に処理されました。ホットリロードも効いていますので、ソースコードの変更は即座に反映されます。

Information

ここでは記載しませんでしたが、REST APIだけでなく、ストレージやキャッシュ、静的リソースのサポートもあります。
必要に応じて参照してください。

ローカルでビルド・実行する

#

まずは、ローカル環境でこのAPIをビルドしてみます。
以下のコマンドを実行します。

npx nitropack build

デフォルトは.outputディレクトリ配下にビルド成果物が出力されます。
ここでは、.output配下は以下のようになりました。

.output
├── nitro.json
├── public
└── server
    ├── chunks
    │ ├── foo.get.mjs
    │ ├── foo.get.mjs.map
    │ ├── foo.post.mjs
    │ ├── foo.post.mjs.map
    │ └── nitro
    │     ├── node-server.mjs
    │     └── node-server.mjs.map
    ├── index.mjs
    ├── index.mjs.map
    ├── node_modules
    │ ├── buffer-from
    │ ├── (...省略)
    └── package.json

Nitroはバンドラーとしてrollupを使い、Tree Shakingで必要なもののみをバンドルします。
このため、プロジェクト配下のnode_modulesディレクトリはデプロイに不要です。
.outputディレクトリのみがデプロイ対象となりますので、かなり軽量になります。

デフォルトではNode.js Server向けにビルドされます。これをローカル環境で起動する場合は、以下のようになります。

node .output/server/index.mjs

Node.js Serverが起動します。これで先程同様にcurl等でAPIにアクセスできます。
コンテナ等でNitroを動かす場合は、これを利用することになるかと思います。

Information

ビルド設定は、nitro.config.tsを別途作成することで、環境によってカスタマイズできます。
例えば、バンドルをミニファイする場合は以下のように指定します。

import { defineNitroConfig } from 'nitropack'

export default defineNitroConfig({
minify: true
})

設定の詳細は、以下公式ドキュメントを参照してください。

Nitro REST APIをAWS Lambdaにデプロイする

#

前述の通り、Nitroはマルチプロバイダーのサーバーレス環境で動かすことができます。
先程はデフォルトのNode.js Serverで動かしましたが、今度はAWS Lambdaにデプロイしてみます。

Lambdaにデプロイする場合は、ビルド時にpresetを指定します。

NITRO_PRESET=aws-lambda npx nitropack build

環境変数NITRO_PRESETaws-lambdaを指定します[1]。このようにすることで、NitroはLambda用の実行コードを出力します。
Lambda以外の場合もこのpresetを変更することで、プロバイダーに応じたビルド結果へ切り替えできるようになっています。
なお、Netlify等の一部のプロバイダーではpresetを使わなくても自動検知可能です。
プロバイダー設定の詳細は公式ドキュメントを参照してください。

ビルド結果を見ると、先程と少し変わっています。
.output/server/chunksの中を見ると以下のようになりました。

.output/server/chunks/
├── foo.get.mjs
├── foo.get.mjs.map
├── foo.post.mjs
├── foo.post.mjs.map
└── nitro
    ├── aws-lambda.mjs
    └── aws-lambda.mjs.map

nitro配下がNode.js ServerからLambda向けに切り替わりました。
実装内容は省略しますが、この中のaws-lambda.mjsを見るとLambda特有のリクエスト、レスポンスフォーマットを処理するようになっていました。

これをAWSにデプロイします。
ここでは、LambdaのデプロイにServerless Frameworkを使います。プロジェクトルート直下にserverless.yamlを用意しました。

service: nitro-sample
provider:
name: aws
runtime: nodejs16.x
region: ap-northeast-1
package:
patterns:
- "!**"
- ".output/**"
functions:
foo:
handler: .output/server/index.handler
url: true # Lambda Function URLを使う

非常にシンプルな構成にしました。
.output配下のファイルのみをデプロイ対象とし、functionsには単一のLambda関数(foo)を定義しています。
Lambda関数のハンドラーには、Nitroで出力したエントリーポイントを指定します。
また、url: trueとしてAPI Gatewayを配置せずに、Lambda Function URL経由[2]でLambda関数へアクセスできるようにしました。

npx serverless deploy

AWS CLIでLambadaのURLを確認し、curlでアクセスしてみます。 URLはデプロイ時の出力やマネジメントコンソールからの確認でも構いません。

# AWS CLI
LAMBDA_URL=$(aws lambda get-function-url-config --function-name nitro-sample-dev-foo --query FunctionUrl --output text)
> https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/

curl ${LAMBDA_URL}foo?name=mamezou
> GET: mamezou

curl ${LAMBDA_URL}foo -d '{"name": "mamezou"}' -H 'Content-Type: application/json'
> POST: mamezou

Node.js Serverと同じように、AWS LambdaでNitroのAPIサーバーが使えるようになりました。

まとめ

#

今回は純粋にNitroを使って、APIサーバーを作成してみました。非常にシンプルで使いやすく、Nuxt3を使わずともこれ単体でもいろいろできそうだなと思いました。

ちなみに、今回Lambdaのコールドスタートにかかった時間は240msほどでした。
ミニファイ設定等を調整すればもう少し短縮できるかもしれませんが、まずまずの数値だと思います。
とはいえ、今回はシンプルすぎるAPIでNitro以外の依存関係もないので、実用レベルになるともっとかかりそうです。

この辺りの時間が許容できなくなった場合は、その時にコンテナ等で常時起動させておくように変更すれば良さそうです。
というのも今回実装したREST APIでLambdaに依存するコードは一切なく、Nitroのビルド時にパラメータレベルで切り替えしました。
ユニバーサルサーバーのNitroは可搬性に優れていますので、臨機応変にデプロイ先を変えていくことができそうです。


参考資料


  1. presetの指定は設定ファイル(nitro.config.ts)でも可能です。 ↩︎

  2. Lambda Function URLはこちらの記事で紹介していますので、必要な場合はご参照ください。 ↩︎

豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)
  2. 自然言語処理初心者が「GPT2-japanese」で遊んでみた (2022-07-08)
  3. GitHub Codespaces を使いはじめる (2022-05-18)
  4. Jest再入門 - 関数・モジュールモック編 (2022-07-03)
  5. ORマッパーのTypeORMをTypeScriptで使う (2022-07-27)
  6. Nuxt3入門(第4回) - Nuxtのルーティングを理解する (2022-10-09)
  7. Nuxt3入門(第3回) - ユニバーサルフェッチでデータを取得する (2022-10-06)
  8. 第1回 OpenAPI Generator を使ったコード生成 (2022-06-04)
  9. Nuxt3入門(第8回) - Nuxt3のuseStateでコンポーネント間で状態を共有する (2022-10-28)
  10. Nuxt3入門(第2回) - 簡単なNuxtアプリケーションを作成する (2022-10-02)