2024 年 6 月15 日に、ふと AWS Lambda で Node.js の Lambda 関数について深掘りしてみたいなと思い、いくつかのドキュメントを参照してみると AWS Lambda のラインタイム Node.js 16 の Deprecation date が 2024 年 6 月 12 日 であると AWS Lambda の開発者ガイドに記載されていることに気づきました。
これも何かの縁、虫の知らせかと思い、AWS Lambda の Node.js について記事を書くことにしました。
今回は、モジュールの取り扱い方についてまとめていきます。
AWS Lambda では、Node.js 14 以降のラインタイムから ECMAScript (ES) モジュール を使用できます。
ただし、AWS マネジメントコンソールでランタイムが Node.js 16 の Lambda 関数を作成した場合、生成されるコードやそのファイルは CommonJS モジュールとして扱われます。
ES モジュールとして扱うか、 CommonJS モジュールとして扱うかでは、Lambda 関数のコードの書き方も異なるため注意が必要です。
この辺の理解を自分なりに整理したいと思ったので、簡単な検証をしてみました。
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 の指定は問題ないかを確認したほうが良さそうですね!