LocalStack + Terraform + Node.js Lambda が「Cannot find module 'index'」でハマった話

December 2, 2025

はじめに

LocalStack + Terraform + Node.js Lambda で Lambda をデプロイしたときに、以下のエラーでハマりました。

/var/runtime/index.mjs
Cannot find module 'index'

結論から言うと、原因は Terraform の archive_file ではなく、LocalStack が ZIP 内の index.js を ESM と誤判定すること です。

発生したエラー

  • 端末で手動で ZIP 化した場合は正常に Lambda が動作
  • sam local でも AWS 本番環境でも正常
  • Terraform で archive_file を使って作った ZIP を LocalStack にデプロイするとエラー

つまり、ZIP の中身や展開自体は問題ではなく、LocalStack がモジュール形式を判定する段階で誤っている のが原因です。

原因

LocalStack の Node.js 20 ランタイム(あるいは内部の ZIP 処理)が、CommonJS の index.js を ESM と誤判定して index.mjs を読み込もうとするため発生します。

結果として以下のようなエラーが出ます。

Require stack:

  • /var/runtime/index.mjs Cannot find module ‘index’

解決策

この問題を回避するには、以下の方法があります

  • AWS CLI 経由で ZIP を直接アップロードする
  • esbuild で CommonJS としてビルドして、zip 化する
  • LocalStack の Node ランタイム修正を待つ

実装例

Terraform では ZIP を読むだけにする Terraform に ZIP を作らせず、自前でビルドスクリプトで ZIP を作る方法が安全です。

  • ビルドスクリプト
const esbuild = require("esbuild");
const { zip } = require("zip-a-folder");

(async () => {
    await esbuild.build({
        entryPoints: ["src/index.ts"],
        bundle: true,
        minify: false,
        platform: "node",
        target: "es2021",
        outfile: "dist/index.js",
    });

    await zip("dist", "build/payment_initiator.zip");
})();
  • Terraform
resource "aws_lambda_function" "payment_initiator" {
filename = "${path.module}/build/payment_initiator.zip"
  source_code_hash = filebase64sha256("${path.module}/build/payment_initiator.zip")
handler = "index.handler"
runtime = "nodejs20.x"
role = aws_iam_role.lambda_role.arn

depends_on = [aws_iam_role_policy.lambda_ddb_policy]
}

まとめ

Cannot find module ‘index’ は LocalStack の Node.js ランタイムによる ESM 誤判定バグ Terraform では ZIP を作らせず、ビルドスクリプトで ZIP を作るのがシンプル

Nifty tech tag lists from Wouter Beeftink