0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Node.js 14→22】Lambdaランタイム更新で遭遇したつまづいた点と解決策まとめ (Middy/TypeORM/AWS SDK)

0
Last updated at Posted at 2025-12-26

はじめに

AWS LambdaNode.js 14ランタイムのサポート終了に伴い、一気に Node.js 22 へアップデートを実施するプロジェクトに参加した。
8年分(v14→v22)なので、依存ライブラリもメジャーバージョンアップが必須となり、数多くの破壊的変更に遭遇した。

本記事では、特にハマりどころが多かった以下のライブラリ移行について、具体的なエラーと解決策をまとめた。

0. 依存パッケージの一括アップデート

本題の前に、パッケージのアップデートに便利なコマンドがあったので、紹介しておく。
今回のPJでは、ディレクトリごとに多数の package.json が存在したので、それぞれのディレクトリでnpm installしていくのは、非常に面倒だった。
そこで、以下のワンライナーを使えば、プロジェクト配下の全てのpackage.jsonを検出し、依存関係を最新化してインストールできるので、非常に便利だった。

find . -name "package.json" -not -path "*/node_modules/*" -execdir sh -c 'ncu -u && npm install' \;

package.json:アプリの設計図

アプリを動かすために必要なツール(ライブラリ)の名前とバージョンが書かれたメモ帳のようなもの。
これがあるおかげで、誰が作業しても同じ環境を再現できる。

npm install:ツールの自動準備

package.jsonに書かれたリストを読み取り、インターネット上から必要なツールをすべて自動でダウンロードして、自分のPCの中に保存(node_modulesフォルダに格納)するコマンド。

コマンドの解説

  • find . -name "package.json": カレントディレクトリ以下のpackage.jsonを探す
  • -not -path "*/node_modules/*": node_modules 内のファイルは除外する
  • -execdir ... \;: 見つかったディレクトリに移動してコマンドを実行する
  • ncu -u: npm-check-updatespackage.jsonのバージョン表記を最新に書き換える
  • npm install: 書き換えた内容でインストールし、package-lock.jsonを更新する

1. TypeORM (v0.2 → v0.3) のマイグレーション

TypeORM 0.3ではDB接続のアーキテクチャが刷新された。
Connection が廃止され DataSource が導入された。
これに伴い CLI の挙動も変わっていた。

発生したエラー

マイグレーション実行コマンドであるnpm run migrateを実行すると以下のエラーが発生した。

Error: Given data source file must contain export of a DataSource instance

原因と対策

CLIのオプションと設定ファイルの書き方が変更されているので、書き方を更新する必要があった。

1. package.json (scripts) の修正

設定ファイルを指定するオプションが -f から -d (DataSource) に変わった

// Before (v0.2)
"migrate": "typeorm -f ./ormconfig.js migration:run"

// After (v0.3)
"migrate": "typeorm -d ./ormconfig.js migration:run"

2. ormconfig.js の修正

以前は設定オブジェクト(JSON)を export していたが、v0.3以降はDataSourceインスタンスを export する様に修正する必要があった

// Before(v0.2)
module.exports = {
  type: "mysql",
  host: process.env.DB_HOST,
  // ...
};

// After(v0.3)
const { DataSource } = require("typeorm");

const dataSource = new DataSource({
   type: "mysql",
   host: process.env.DB_HOST,
   // ...
});

// インスタンスをexportする
module.exports = dataSource;

2. Middy (v2 → v6) をCommonJS環境で適用

今プロジェクトの方針として CommonJS (require) のまま維持しつつ、Pure ESM になった最新の Middy v6 を導入する必要があった。

補足

  • CommonJS (CJS): 昔からあるルールで、require() で呼ぶ
  • ESM: 最近の標準ルールで、import で呼ぶ

直面した課題

「昔のルール(CJS)」のファイルの中で、最新の「ESM専用ツール」を require しようとすると、「ルールが違うので読み込めない」 とエラーが出て動かない

Error: require() of ES Module ... not supported

解決策:Dynamic Import と Wrapper パターン

本来は、ファイル全体を ESM (.mjs) に書き換えるのが正攻法だが、テストコードやビルド設定への影響を考慮すると、Lambda関数ファイルの修正は最小限にしたかった。
そこで、「基本は古いルールのままだが、使う瞬間だけ新しいルールで呼び出す」 という手法をとる必要があった。これが 「Dynamic Import(動的インポート)」 である。
そこで、Lambdaハンドラ関数の中で import() する 方法を採用することで、既存のrequireを使用した構成を崩さずに、最新の ESM ライブラリを利用できた。

// ❌ Top-levelでのrequireは不可
// const middy = require('@middy/core');

const baseHandler = async (event) => { /* ロジック */ };

// ハンドラをasync関数でラップする
const handler = async (event, context) => {
  // 1. 関数内で動的にimport (ここならCJSファイルでも書ける)
  //    .default で本体を取り出すのがポイント
  const { default: middy } = await import('@middy/core');
  const { default: jsonBodyParser } = await import('@middy/http-json-body-parser');

  // 2. Middyインスタンスを生成
  //    v6では .before() メソッド等は廃止され、.use() チェーンに統一されている点に注意
  const middyHandler = middy(baseHandler)
    .use({
        before: async (request) => { /* 前処理 */ }
    })
    .use(jsonBodyParser());

  // 3. 生成したハンドラを実行して結果を返す
  return middyHandler(event, context);
};

module.exports = { handler };

3. AWS SDK (v2 → v3) により発生した「署名検証エラー」

基本的な書き換え(v2 vs v3)

モジュラーインポート

v2では aws-sdk 全体を読み込んでいたが、v3では必要なパッケージだけをインストール・インポートすることで、これによりLambdaのサイズが軽量化される仕様である。

// v2
const AWS = require('aws-sdk');
const s3 = new AWS.S3();

// v3
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const s3 = new S3Client({});

Commandパターンの採用と .promise() の廃止

v3の最大の特徴は send(new Command()) パターン。
また、.promise() がなくなり、標準で Promise が返されるようになった。

// v2
const result = await s3.getObject(params).promise();

// v3
const command = new GetObjectCommand(params);
const result = await s3.send(command);

事象:KMSInvalidSignatureException

SDK v3への移行はモジュラーインポートなどが注目されがちだが、バイナリデータの型変更によるバグに遭遇した。KMSで作成したJWT署名を検証しようとすると、データは合っているはずなのに「KMSInvalidSignatureException」という署名不一致エラーが発生した。

エラー調査

当初は 「KMSInvalidSignatureException」 というエラー名から、「検証側(Lambda)でのBase64デコード処理で、パディング(=) の計算が間違っている?」と疑ったが解消しなかった。そこで、検証側のLambdaに、KMSへ渡す直前のデータをすべて出力するログを仕込んだ。特に重要だったのは、署名データの「見た目」と「バイト長」

// 検証側 (Lambda Authorizer) のデバッグログ実装例
const signatureSegment = segments.pop(); // JWTから取り出した署名部分
console.log(signatureSegment); // 1. どんな文字列が来ているか?

const signature = decodeBase64Url(signatureSegment); // デコード後のバイナリ
console.log(signature.length); // 2. バイナリのサイズは何バイトか?

実際に出力されたログ

INFO Signature String: 17,234,140,238,93,156... (延々と続く数字とカンマ)
INFO Signature Length: 498

このログから、

  • 長さがおかしい: 本来256バイトのはずが、498バイトもある
  • 見た目がおかしい: Base64文字列ではなく、**「カンマ区切りの数字の文字列」**になっている

これは、配列を無理やり文字列化(toString)したときの挙動だった

const arr = [17, 234, 140];
console.log(arr.toString()); 
// 出力: "17,234,140"

原因:戻り値が Buffer ではなく Uint8Array

v3での発行側では行われてた処理

  1. KMSの署名作成コマンドが、v3では Buffer ではなく Uint8Array(バイト配列) を返すようになった
  2. Uint8Array には .toString('base64') メソッドが存在しない
  3. 引数の'base64'が無視され、デフォルトの .toString() が発動
  4. バイナリデータがBase64エンコードされず、「中身の数字をカンマで繋いだテキスト」 としてトークンに埋め込まれた

結論

検証側は、この 「中身の数字をカンマで繋いだテキスト」 を無理矢理デコードした結果としてサイズ違い(498バイト)の壊れたデータになり、KMSに 署名不一致エラー として弾かれていた。

解決策

  • SDK v2ではバイナリデータはBufferが返ってきた
  • SDK v3ではブラウザ互換性のために標準のUint8Array が返る
  • Buffer には .toString('base64') メソッドがあるが、Uint8Array には無い
  • JavaScriptの仕様上、引数を無視して Object.toString() のような挙動(要素をカンマ区切りにした文字列)をする

そこで、Buffer.from() で明示的にラップしてから Base64 変換を行った

// v2 (Bufferが返る)
const signature = res.Signature.toString('base64'); // OK

// v3 (Uint8Arrayが返る)
// ❌ そのままtoStringすると数字の羅列になる
// const signature = res.Signature.toString('base64'); 

// ⭕️ Bufferに変換してから処理する
const signature = Buffer.from(res.Signature).toString('base64');

例え話:「翻訳ミス」

v2(ベテラン): > 「このデータを Base64 で書いて」と言えば、ちゃんと翻訳してくれる
v3(新人): > Base64 という言葉を知らないので、手元の数字をそのまま書き写して(17,234...) 渡してきた。なので、受け取った側(KMS)は「英語じゃないから読めないよ!」と怒ってエラーする

まとめ

  1. CLIツールの変更: TypeORM のように、オプションや設定ファイルの作法がガラッと変わることがある
  2. ESM対応: ライブラリが ESM Only になっている場合、import() を活用するか、プロジェクト自体の ESM 化を検討する
  3. 型と戻り値の確認: SDK v3 の Uint8Array のように、静かに挙動が変わっているものがある
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?