のべラボ.blog

Tech Blog | AWS や サーバーレスやコンテナ などなど

Node.js の AWS Lambda 関数におけるモジュールの扱いについて

2024 年 6 月15 日に、ふと AWS Lambda で Node.js の Lambda 関数について深掘りしてみたいなと思い、いくつかのドキュメントを参照してみると AWS Lambda のラインタイム Node.js 16 の Deprecation date が 2024 年 6 月 12 日 であると AWS Lambda の開発者ガイドに記載されていることに気づきました。

docs.aws.amazon.com

これも何かの縁、虫の知らせかと思い、AWS Lambda の Node.js について記事を書くことにしました。

今回は、モジュールの取り扱い方についてまとめていきます。

AWS Lambda では、Node.js 14 以降のラインタイムから ECMAScript (ES) モジュール を使用できます。

ただし、AWS マネジメントコンソールでランタイムが Node.js 16 の Lambda 関数を作成した場合、生成されるコードやそのファイルは CommonJS モジュールとして扱われます。

ES モジュールとして扱うか、 CommonJS モジュールとして扱うかでは、Lambda 関数のコードの書き方も異なるため注意が必要です。

docs.aws.amazon.com

aws.amazon.com

この辺の理解を自分なりに整理したいと思ったので、簡単な検証をしてみました。

AWS マネジメントコンソールで、ランタイムが Node.js 16 と 18 の Lambda 関数を作成し、それぞれ、どうすれば ES モジュールとして扱われるか、または CommonJS モジュールとして扱われるかを確認していきます。

準備:AWS マネジメントコンソールから Node.js の Lambda 関数を作成

ランタイムに Node.js 16 を指定して作成した場合

  • 下記のコードを含んだ index.js が生成されます。

このデフォルトのコードを、この記事では便宜上、CommonJS モジュールのコード と呼びます。

これは、もちろん問題なく実行できます。

exports.handler = async (event) => {
  // TODO implement
  const response = {
    statusCode: 200,
    body: JSON.stringify('Hello from Lambda!'),
  };
  return response;
};

ランタイムに Node.js 18 または 20 を指定して作成した場合

  • 下記のコードを含んだ index.mjs が生成されます。

このデフォルトのコードを、この記事では便宜上、ES モジュールのコード と呼びます。

これも、問題なく実行できます。

export const handler = async (event) => {
  // TODO implement
  const response = {
    statusCode: 200,
    body: JSON.stringify('Hello from Lambda!'),
  };
  return response;
};

では、これらのコードを使って検証していきます。


検証 1. ランタイムが Node.js 18 の Lambda 関数 の index.mjs に、CommonJS モジュールのコード で実行してみる

package.json ファイルは無しの状態です。

  • 結果、エラーになります。

これは、AWS Lambda では拡張子が .mjs のファイルを ES モジュールとして扱うためです。

{
  "errorType": "ReferenceError",
  "errorMessage": "exports is not defined in ES module scope",
  "trace": [
    "ReferenceError: exports is not defined in ES module scope",
    "    at file:///var/task/index.mjs:1:1",
    "    at ModuleJob.run (node:internal/modules/esm/module_job:195:25)",
    "    at async ModuleLoader.import (node:internal/modules/esm/loader:337:24)",
    "    at async _tryAwaitImport (file:///var/runtime/index.mjs:1008:16)",
    "    at async _tryRequire (file:///var/runtime/index.mjs:1057:86)",
    "    at async _loadUserApp (file:///var/runtime/index.mjs:1081:16)",
    "    at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1119:21)",
    "    at async start (file:///var/runtime/index.mjs:1282:23)",
    "    at async file:///var/runtime/index.mjs:1288:1"
  ]
}

検証 2. ランタイムが Node.js 18 でファイル名を index.js にして、CommonJS モジュールのコードで実行してみる

package.json ファイルは無しの状態です。

  • 結果、正常に実行されます。

これは、AWS Lambda では デフォルトでは拡張子が .js のファイルを CommonJS モジュールとして扱うためです。


検証 3. ランタイムが Node.js 18 でファイル名を index.cjs にして、CommonJS モジュールのコードで実行してみる

package.json ファイルは無しの状態です。

  • 結果、正常に実行されます。

これは、AWS Lambda では拡張子が .cjs のファイルを CommonJS モジュールとして扱うためです。


検証 4. ランタイムが Node.js 16 の Lambda 関数 の index.js に、ES モジュールのコードで実行してみる

package.json ファイルは無しの状態です。

  • 結果、エラーになります。

これは、AWS Lambda では デフォルトでは拡張子が .js のファイルを CommonJS モジュールとして扱うためです。

{
  "errorType": "Runtime.UserCodeSyntaxError",
  "errorMessage": "SyntaxError: Unexpected token 'export'",
  "trace": [
    "Runtime.UserCodeSyntaxError: SyntaxError: Unexpected token 'export'",
    "    at _loadUserApp (file:///var/runtime/index.mjs:1084:17)",
    "    at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1119:21)",
    "    at async start (file:///var/runtime/index.mjs:1282:23)",
    "    at async file:///var/runtime/index.mjs:1288:1"
  ]
}

ただし、package.json ファイルで type を module として指定することで、.js ファイルを ES モジュールとして扱うこともできます。

次の検証 5. で確認してみましょう。


検証 5. ランタイムが Node.js 16 の index.js に、ES モジュールのコードを記述し、package.json ファイルで type を module に指定して実行する

package.json の内容

{
  "name": "example",
  "type": "module",
  "description": "This package will be treated as an ES module.",
  "version": "1.0",
  "main": "index.js"
}
  • 結果、正常に実行されます。

拡張子が .js の場合は、package.json の type の指定で制御できることがわかりました。

では、拡張子が .cjs の場合はどうでしょう?次の検証 6. で確認してみます。


検証 6. ランタイムが Node.js 16 でファイル名を index.cjs にして、ES モジュールのコードを記述し、package.json ファイルで type に module を指定して実行する

package.json の内容

{
  "name": "example",
  "type": "module",
  "description": "test6",
  "version": "1.0",
  "main": "index.cjs"
}
  • 結果、エラーになります。

これは、AWS Lambda では拡張子が .cjs のファイルを CommonJS モジュールとして扱うためです。.cjs の場合は、package.json で type に module を指定しても、ES モジュールとは扱われません。

{
  "errorType": "Runtime.UserCodeSyntaxError",
  "errorMessage": "SyntaxError: Unexpected token 'export'",
  "trace": [
    "Runtime.UserCodeSyntaxError: SyntaxError: Unexpected token 'export'",
    "    at _loadUserApp (file:///var/runtime/index.mjs:1084:17)",
    "    at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1119:21)",
    "    at async start (file:///var/runtime/index.mjs:1282:23)",
    "    at async file:///var/runtime/index.mjs:1288:1"
  ]
}

検証 7. ランタイムが Node.js 16 でファイル名を index.mjs にして、ES モジュールのコードで実行してみる

package.json ファイルは無しの状態です。

  • 結果、正常に実行されます。

これは、AWS Lambda では拡張子が .mjs のファイルを ES モジュールとして扱うためです。

package.json ファイルの type には、module だけでなく commonjs という値を指定できます。

では、次の検証 8. のパターンで確認してみましょう。


検証 8. ランタイムが Node.js 18 の Lambda 関数 の index.mjs に、CommonJS モジュールのコードを記述し、package.json ファイルで type に commonjs を指定して実行する

package.json の内容

{
  "name": "example",
  "type": "commonjs",
  "description": "test.",
  "version": "1.0",
  "main": "index.mjs"
}
  • 結果、エラーになります。

これは、AWS Lambda では拡張子が .mjs のファイルを ES モジュールとして扱うためです。.mjs の場合は、package.json で type に commonjs を指定しても、CommonJS モジュールとは扱われません。

{
  "errorType": "ReferenceError",
  "errorMessage": "exports is not defined in ES module scope",
  "trace": [
    "ReferenceError: exports is not defined in ES module scope",
    "    at file:///var/task/index.mjs:1:1",
    "    at ModuleJob.run (node:internal/modules/esm/module_job:195:25)",
    "    at async ModuleLoader.import (node:internal/modules/esm/loader:337:24)",
    "    at async _tryAwaitImport (file:///var/runtime/index.mjs:1008:16)",
    "    at async _tryRequire (file:///var/runtime/index.mjs:1057:86)",
    "    at async _loadUserApp (file:///var/runtime/index.mjs:1081:16)",
    "    at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1119:21)",
    "    at async start (file:///var/runtime/index.mjs:1282:23)",
    "    at async file:///var/runtime/index.mjs:1288:1"
  ]
}

つまり、package.json ファイルの type の指定で制御できるのは、拡張子が .js のときだけということがわかりました。

package.json ファイルが無い場合、つまりデフォルトだと CommonJS モジュールとして扱われるので、拡張子が .js の場合で、ES モジュールとして扱いたい場合のみ package.json ファイルを作成し、type に module を指定すればよい、ということになります。


ここまでの整理

検証パターン数が多くなったので、ここでひとまず整理します。

まず、AWS マネジメントコンソールから Lambda 関数を作成した場合は下表のようになり、ランタイムにより異なります。

ランタイム Node.js 16 Node.js 18 以降
生成されるファイル index.js index.mjs
生成されるコード CommonJS モジュールを想定したコード ES モジュールを想定したコード

ただ、コードを CommonJS モジュールとして扱うか、ES モジュールとして扱うかは下表のようになります。

これは、Node.js 16 でも Node.js 18 以降でも変わりません。(厳密には Node.js 14 から現時点の最新の Node.js 20 までは同じです。)

ファイル拡張子 モジュールの扱い方
.mjs ES モジュールとして扱う
.cjs CommonJS モジュールとして扱う
.js デフォルトでは CommonJS モジュールとして扱う。ただし package.json で type に module を指定すれば ES モジュールとして扱う

最後に

今回の記事のポイントは、下記になります。

  • Lambda 関数のコードのファイルの拡張子により、モジュールの扱いが変わってくる
    • 拡張子が .js の場合は、package.json の type の指定で制御可能
  • AWS マネジメントコンソール で Lambda 関数を作成する場合、ランタイムが Node.js 16 の場合と、18 移行の場合で生成されるファイルの拡張子が異なる

これから新規のコードを書く場合は特に問題ないかもしれませんが、古くから公開されている各種サンプルのコードを使う場合は、CommonJS モジュールか、ES モジュールか、ファイルの拡張子は何か、package.json での type の指定は問題ないかを確認したほうが良さそうですね!


/* -----codeの行番号----- */