AWS Transform Customを使ってAWS Lambda Node.js 20のランタイムを22に移行してみた

AWS Transform Customを使ってAWS Lambda Node.js 20のランタイムを22に移行してみた

2026.05.10

こんにちは、せーのです。

Lambda の nodejs20.x は、2026年4月30日をもって非推奨(Deprecation)に入りました。記事を書いている時点ではその日はもう過ぎています。セキュリティパッチが止まるフェーズに入った、という意味では「動いているから安心」とは言えなくなっています。さらに 2026年8月31日 には nodejs20.x での新規関数作成がブロックされ、9月30日 には更新までブロックされます。

https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html

そこで「AWS Transform Custom のマネージド変換 AWS/nodejs-version-upgrade を使えば、リポジトリ単位で Node.js 22 に寄せられるのでは」という話を聞き、実際にどこまで自動でやってくれるか試しました。SAM のチュートリアル級ではなく、わざと AWS SDK v2 を載せた Lambda を用意して、ランタイム更新と SDK 移行の両方をお願いしてみましょう。

結論から言うと、まず AWS Transform Custom だけで 約10分で変換ブランチができ、nodejs22.x と SDK v3 まで一気に直せました。続けて Lambda 実機に zip を載せ替えて変換前後を突き合わせたところ、同一イベントでは レスポンス本文まで完全一致することを確認できました。あわせて副産物として、デプロイ zip は約 73% 削減(約 14MB → 約 3.7MB)、CloudWatch の **Init Duration は約 43% 短縮(約 685ms → 約 393ms)**といった数字も取れました(いずれも検証用の最小関数・単発計測なので「常にこうなる」とは限りません)。Transform 実行時は 認証まわりと -g の指定で最初はコケる ので、そのへんも本文に残しています。

Lambda の Node.js 20.x はいまどうなっているか

非推奨後も Invoke 自体は止まりませんが、パッチなし・サポート対象外になります。移行先としては現時点では nodejs22.x が無難で、nodejs24.x はコールバック型ハンドラーが使えなくなるなどの制約があるので要注意です。

フェーズ 日付 何が起きるか
Deprecation(非推奨) 2026年4月30日(既に経過) セキュリティパッチ停止、Lambda技術サポート対象外
Block function create 2026年8月31日 nodejs20.xでの新規Lambda関数作成が不可能に
Block function update 2026年9月30日 nodejs20.xを使う関数の更新(コード・設定変更)が不可能に
ランタイム 識別子 非推奨予定 推奨度
Node.js 22 nodejs22.x 2027年4月30日 ◎(現時点の推奨移行先)
Node.js 24 nodejs24.x 2028年4月30日 △(コールバック型ハンドラー非サポートのため要注意)

自分のアカウントで nodejs20.x を棚卸しする

まずは影響範囲を数えるのが早いです。

aws lambda list-functions \
  --query 'Functions[?Runtime==`nodejs20.x`].[FunctionName,Runtime,LastModified]' \
  --output table

全リージョンをざっと見るなら次のようにループします。

for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  echo "=== $region ==="
  aws lambda list-functions \
    --region $region \
    --query 'Functions[?Runtime==`nodejs20.x`].FunctionName' \
    --output text
done

Node.js 22 に上げるときの注意点

Transform だけでは救えないものもあります。

  • ネイティブモジュールsharp / bcrypt / canvas など)は ABI が変わるので再ビルドが必要で、自動変換だけでは完結しません。
  • RDS など SSL を伴う接続では NODE_EXTRA_CA_CERTS のような環境変数が必要なケースがあり、IaC を書き換えてくれてもそこまで自動とは限りません。
  • ストリームのデフォルト high water mark が Node.js 22 で変わるため、メモリ小さめの Lambda でストリーム処理していると挙動差が出ることがあります。
  • AWS SDK v2 はサポート終了済み(AWS の告知: JavaScript SDK v2 の End-of-Support、ライフサイクルは AWS SDKs and Tools のメンテナンスポリシー に準拠)なので、ついでに v3 に寄せたほうがよい、というのが今回の実験の動機のひとつでもあります。

AWS Transform Custom とは(おさらい)

AWS Transform Custom は、エージェントがリポジトリを読んで計画・編集・検証まで行う仕組みです。CLI は atx、今回使ったのはレジストリ上のマネージド定義 AWS/nodejs-version-upgrade です。

前提として Git でコミット済みのリポジトリであること が必須です(Getting Started の前提条件 で「作業ディレクトリが有効な Git リポジトリであること」と初回コミットまでの例が書かれています。Troubleshooting にも、リポジトリが Git のソース管理下にあることが前提だと書かれています)。変換結果は atx-result-staging-* のようなローカルのステージング用ブランチ にコミットされます(AWS DevOps Blog の手順 で「ローカルのステージングブランチ」と説明され、git diff main <atx-result-staging-...> として確認する例が載っています)。料金はエージェント稼働に対して 1分あたり 0.035 USD という単価です。CLI の認証は 環境変数や ~/.aws/credentials の通常の IAM 認証情報 で動かす想定で、Web コンソール側のキャンペーン管理とは SSO 要件が違う点に注意です。

公式の CLI インストールは次のスクリプトです。

curl -fsSL https://desktop-release.transform.us-east-1.api.aws/install.sh | bash
atx --version

やってみた

何を試したか

次の 2 段です。

  1. Transform Custom: 意図的に AWS SDK v2(aws-sdk)を載せた Node.js 20 の Lambda を用意し、マネージド変換 AWS/nodejs-version-upgradeNode.js 22 + SDK v3(@aws-sdk/client-s3 に寄せられるか見る。
  2. Lambda 実機での突き合わせ: 変換 のコードと のコードを、それぞれ zip で別関数としてデプロイし、同一のイベントと同一の S3 オブジェクトに対して aws lambda invoke。返ってきた ペイロード JSON と CloudWatch の REPORT 行を並べて比較する。

※ アカウント ID・実バケット名・関数名・リクエスト ID などはマスクまたは代表値です。フルのコマンドログは aws-transform-custom/hands-on.md にあります。


変換前のリポジトリ(実験の出発点)

Git でコミット済みのディレクトリに、次の 3 ファイルだけの最小サンプルを置きました(BUCKET_NAME は環境変数、event.key を S3 のキーとして GetObject して本文を返す)。

index.js(SDK v2)

'use strict';
const AWS = require('aws-sdk');
const s3 = new AWS.S3();

exports.handler = async function(event, context) {
  const params = {
    Bucket: process.env.BUCKET_NAME,
    Key: event.key
  };

  try {
    const data = await s3.getObject(params).promise();
    return {
      statusCode: 200,
      body: data.Body.toString('utf-8')
    };
  } catch (err) {
    console.error('Error:', err);
    throw err;
  }
};

package.json

{
  "name": "nodejs20-sdk-v2-sample",
  "version": "1.0.0",
  "description": "Node.js 20 Lambda with AWS SDK v2",
  "main": "index.js",
  "engines": { "node": "20" },
  "dependencies": {
    "aws-sdk": "^2.1691.0"
  }
}

template.yaml(抜粋)

      Handler: index.handler
      Runtime: nodejs20.x
      Environment:
        Variables:
          BUCKET_NAME: my-test-bucket

Transform で実行したコマンド(非対話)

ローカルでは IDE 連携の login_session 型プロファイルatx が読めないことがあったため、aws configure export-credentials --format env で環境変数に出してから実行しました。また additionalPlanContext にスペースが含まれるので、-g はインラインではなく JSON ファイルを渡しました(インラインだと Invalid configuration format になった)。

eval $(AWS_PROFILE=demo-profile aws configure export-credentials --format env)
unset AWS_PROFILE
export AWS_REGION=us-east-1

cat > /tmp/atx-config.json << 'EOF'
{
  "additionalPlanContext": "Upgrade from Node.js 20 to Node.js 22, also migrate AWS SDK v2 to AWS SDK v3 if applicable"
}
EOF

atx custom def exec \
  -n AWS/nodejs-version-upgrade \
  -p ~/work/nodejs20-sdk-v2-sample \
  -c "node --version || true" \
  -g "file:///tmp/atx-config.json" \
  -x -t

変換の結果(リポジトリ上)

  • 壁時計で 約 10 分atx-result-staging-* ブランチにコミットが積まれた。
  • エージェントは計画どおり template.yaml の Runtimepackage.jsonengines と依存index.jsSDK 呼び出しを書き換え、npm install なども実行した(詳細は会話ログ)。

変換後のコード(どう変わったか)

index.js(SDK v3)

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

exports.handler = async function(event, context) {
  const params = {
    Bucket: process.env.BUCKET_NAME,
    Key: event.key
  };

  try {
    const data = await s3Client.send(new GetObjectCommand(params));
    return {
      statusCode: 200,
      body: await data.Body.transformToString('utf-8')
    };
  } catch (err) {
    console.error('Error:', err);
    throw err;
  }
};

package.json(依存と engines)

{
  "name": "nodejs20-sdk-v2-sample",
  "version": "1.0.0",
  "description": "Node.js 20 Lambda with AWS SDK v2",
  "main": "index.js",
  "engines": { "node": "22" },
  "dependencies": {
    "@aws-sdk/client-s3": "^3.750.0"
  }
}

template.yaml

Runtime: nodejs20.xnodejs22.x に更新。

変換でここが違う(整理)

観点 変換前 変換後
ランタイム(SAM) nodejs20.x nodejs22.x
engines "20" "22"
依存 モノリシック aws-sdk v2 @aws-sdk/client-s3 のみ(モジュラー v3)
S3 呼び出し new AWS.S3() + getObject().promise() new S3Client() + send(new GetObjectCommand(params))
本文の文字列化 data.Body.toString('utf-8') await data.Body.transformToString('utf-8')

追加実験: Lambda 実機で「同じ入力 → 同じ出力」か確かめる

リポジトリ上で綺麗でも実行時に差が出ることがあるので、変換前ブランチ(master)と変換後ブランチ(atx-result-staging-*)をそれぞれ npm install --omit=dev したうえで

zip -qr /tmp/lambda-pre.zip index.js package.json node_modules   # 変換前
zip -qr /tmp/lambda-post.zip index.js package.json node_modules  # 変換後

のように zip を作り、create-function別名の Lambda 関数を 2 本nodejs20.x / nodejs22.x)作成しました。検証用 S3 に本文 hello-from-transform-verify のオブジェクトを置き、関数環境変数 BUCKET_NAME でそのバケットを渡しています。

投入したイベント(両関数で同一)

{"key": "transform-verify/hello.txt"}

invoke コマンド(例)

echo '{"key": "transform-verify/hello.txt"}' > /tmp/event.json

aws lambda invoke \
  --function-name "verify-transform-pre" \
  --cli-binary-format raw-in-base64-out \
  --payload fileb:///tmp/event.json \
  --log-type Tail \
  /tmp/lambda-verify-node20.json

aws lambda invoke \
  --function-name "verify-transform-post" \
  --cli-binary-format raw-in-base64-out \
  --payload fileb:///tmp/event.json \
  --log-type Tail \
  /tmp/lambda-verify-node22.json

実験結果: 変換前(nodejs20.x + SDK v2)の応答

CLI は StatusCode 200 / FunctionError なし。保存したレスポンスファイルのペイロードは次の形です。

{"statusCode":200,"body":"hello-from-transform-verify\n"}

CloudWatch に出た REPORT 行(1 回の計測・メモリ 256MB)の例:

REPORT RequestId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx  Duration: 629.19 ms  Billed Duration: 1315 ms  Memory Size: 256 MB  Max Memory Used: 96 MB  Init Duration: 684.87 ms

ローカルで見た デプロイ zip約 14MBaws-sdk v2 を丸ごと同梱するため)。

実験結果: 変換後(nodejs22.x + SDK v3)の応答

やはり StatusCode 200 / FunctionError なし。ペイロードは 文字どおり同じです。

{"statusCode":200,"body":"hello-from-transform-verify\n"}

REPORT 行の例:

REPORT RequestId: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy  Duration: 752.15 ms  Billed Duration: 1146 ms  Memory Size: 256 MB  Max Memory Used: 106 MB  Init Duration: 393.07 ms

zip約 3.7MB(v3 は S3 クライアントだけを同梱できたため)。

突き合わせ

diff /tmp/lambda-verify-node20.json /tmp/lambda-verify-node22.json && echo "OK: 完全一致"

実行結果

OK: 完全一致

つまりこの検証では、変換後のロジックでも API として返る JSON は変換前と同一でした。一方で Init Duration は約 685ms → 約 393ms(約 43% 短い)、**zip は約 14MB → 約 3.7MB(約 73% 削減)**といった副産物がありました。Duration(629ms vs 752ms)や Max Memory Used はこの 1 回では変換後の方が大きめなので、「処理時間まで必ず改善」とは限らない点だけは切り分けておきます。


運用で効いた Tips

  • git checkout が止まる: npm install 後に package-lock.json が untracked で残るとブランチ切替に失敗することがある → rm package-lock.json で回避。
  • 認証: atx や検証スクリプトが IDE 連携プロファイルを読めないときは、export-credentials で環境変数に寄せる。

実験サマリ(全体)

項目 結果
Transform(AWS/nodejs-version-upgrade ✅ 約 10 分、nodejs22.x + @aws-sdk/client-s3
変換後コードの差分 上表のとおり(Runtime / engines / SDK API)
Lambda invoke のペイロード 両方 {"statusCode":200,"body":"hello-from-transform-verify\n"}
diff での比較 完全一致
zip サイズ 約 14MB → 約 3.7MB(約 73% 削減)
Init Duration 約 685ms → 約 393ms(約 43% 短縮)

まとめ

今回の最小サンプルでは、AWS/nodejs-version-upgradeNode.js 20→22AWS SDK v2→v3(S3) をまとめてお願いし、ブランチ上では Runtime・engines・ハンドラまで揃うところまでを約 10分で確認しました。エージェント分数の正確な記載はログにないので、Transform の課金は Cost Explorer で突き合わせるのが確実です。

そのうえで 続けて Lambda へ zip デプロイして実 Invoke すると、レスポンスは変換前後で完全一致し、**パッケージは約 73% 軽くなり(約 14MB → 約 3.7MB)、Init Duration は約 43% 短く見えた(約 685ms → 約 393ms)一方、ハンドラの Duration はその 1 回の計測ではむしろ長めでした。オブジェクトが小さく単発なので当てにできない部分もありますが、「軽い zip と Init は気持ちよくても、処理時間まで自動改善とは限らない」**という見方はしておいたほうがよさそうです。

向いているのは「リポジトリ単位でバージョンと依存を揃えたい」「SDK v2 の移行も同じ PR のたたき台に載せたい」といったケースです。向いていないのは ネイティブモジュールだらけのコードベースや、本番の挙動はテストがないと保証できないときの丸投げです。あくまで 生成物は人間がレビューし、本番に近いイベント・データサイズ・IAM・VPC まで含めた検証を自分の環境で通してからマージが前提だと思います。

詳細コマンドと追加実験のステップは aws-transform-custom/hands-on.md にまとめています。

もっとスマートな回り方や、大規模モノレポでの運用ノウハウをお持ちの方がいれば、ぜひ Developers IO のコメントや X などで教えてください。

参考資料

この記事をシェアする

関連記事