はじめに
AWS LambdaのNode.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-updatesで package.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での発行側では行われてた処理
- KMSの署名作成コマンドが、v3では Buffer ではなく Uint8Array(バイト配列) を返すようになった
-
Uint8Array には
.toString('base64')メソッドが存在しない - 引数の
'base64'が無視され、デフォルトの.toString()が発動 - バイナリデータが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)は「英語じゃないから読めないよ!」と怒ってエラーする
まとめ
- CLIツールの変更: TypeORM のように、オプションや設定ファイルの作法がガラッと変わることがある
- ESM対応: ライブラリが ESM Only になっている場合、import() を活用するか、プロジェクト自体の ESM 化を検討する
- 型と戻り値の確認: SDK v3 の Uint8Array のように、静かに挙動が変わっているものがある