Open1

Lambda のランタイム Node.js と AWS SDK for JavaScript のアップデートをした振り返り

nmakinmaki

こんにちは。
久々の投稿ですが、ここしばらくはセキュリティ対策やサポート切れになりそうなコンポーネントのアップデートに手を焼いておりました。本記事ではその要所となった部分をメモとして残します。

Lambda のランタイムを Node.js 20.x へ移行

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtimes.html
16系のサポートは 2024/6/12 までとなっています。
対応時点での最新サポートは20系だったので、そちらで進めました。
開発環境を更新し、 yarn install をやり直し、パッケージをアップロードし、Lambdaの動作設定を Node.js 20.x へ切り替えるといった手順を踏みます。

開発環境の更新

MacBookPro M1 では nodebrew を使って構築しているため、
以下のようなセットアップになりました。

nodebrew install-binary v20.10.0
nodebrew use v20.10.0

手元で都度20系の最新を確認したい場合は、ls-remoteコマンドも併用します。

nodebrew ls-remote

yarn install

node環境を変更したので、更新が必要です。

cd #package.jsonが置いてあるディレクトリ
yarn install

パッケージのアップロード、Lambdaの動作設定

割愛します。AWSのコンソール画面などで実施できます。
コマンドで実施する場合はソースコードのzipをS3に連携させておくと楽です。

Node.js のアップデートは以上でした。幸いにもバージョンを上げて動作しないコンポーネントはなく、大規模なライブラリの組み換え等は考慮不要でした。

AWS SDK for JavaScript v3 へ移行

https://docs.aws.amazon.com/sdkref/latest/guide/maint-policy.html
「メンテナンス期間に入る=サポートされなくなる」ということではないのですが、v2もv1のように数年後にはサポート終了となってしまう可能性が高いため、この機会に省くわけにはいかなかったです。

主にやることとしては、各ライブラリごとに読み込むことと、入出力の違いをラッピングすることでした。

各ライブラリごとに読み込む

丸ごと読み込まない分だけ、容量的には節約できます。
同時に使うコンポーネントの宣言数が多いと、コード量は増えます。

v2

const AWS = require('aws-sdk');
const s3 = new AWS.S3();
const transcribe = new AWS.TranscribeService();

v3

const { S3Client } = require('@aws-sdk/client-s3');
const s3 = new S3Client();
const { TranscribeClient } = require('@aws-sdk/client-transcribe');
const transcribe = new TranscribeClient();

入出力の違いをラッピングする

この部分で気が進まないエンジニアは多いはずです。
各コマンドをモジュールとして読み込む必要があります。
データに関しても、デフォルトのデコードはおろか一括リードにすら対応していません。
チャンク分けしたリードを実行しながらバッファに詰めて、デコードする必要があります。

以下で違いを見ていきましょう。

各コマンドをモジュールとして読み込む

v2ではクライアントオブジェクトがあればメソッドを使用できるので宣言不要でしたが、
v3では以下のような宣言が必要になります。
クライアントオブジェクトのメソッドは、sendを使用します(rubyでsendを用いるとセキュリティで問題になりやすい箇所として散々な言われようになりますが、言語仕様の違いもあるということで一旦信用しておきましょう)。

const { GetObjectCommand, PutObjectCommand, DeleteObjectCommand, ListObjectsV2Command } = require("@aws-sdk/client-s3");
const { StartTranscriptionJobCommand } = require("@aws-sdk/client-transcribe");

s3.send(new GetObjectCommand(options)).then((res) => { ... });
...

チャンク分けしたリード

もともとv3はstreamを直接受け取る設計だったようで、以下のような補助関数を記述する必要があったそうです。

  async function streamToString(stream) {
  return new Promise((resolve, reject) => {
      const chunks = [];
      stream.on('data', (chunk) => chunks.push(chunk));
      stream.on('error', reject);
      stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
    });
  }

↑ ただし、後述の方法を使えば良いので、今では非推奨です。
その後で、BodyオブジェクトにtransformToStringが追加されました。

   s3.send(new GetObjectCommand(options)).then((res) => {
      res.Body?.transformToString('utf-8').then((body) => {
        onSuccess(null, JSON.parse(body));
      }).catch((err) => { onError(err); });
    }).catch((err) => { onError(err); });

もしエンコードがUTF-8以外を想定しなければならない場合は、
一旦バッファとして受け取って、UTF-8に読み替えて記録するような手順になります。

res.Body?.transformToByteArray().then((res) => {
  const encodeFrom = Encoding.detect(res);
  try {
    const buf = new Buffer.from(Encoding.convert(res, { to: "UTF8", from: encodeFrom, type: "array" }));
    const convertedString = buf.toString("utf8");
    /* これまで通りの処理 */
  } catch (e) {
    console.log(e);
    next(e);
  }
}).catch((err) => { onError(err); });

ここで使用しているEncodingは、encoding-japaneseです。UTF32やUTF7以外についてほぼ全ての日本語変換に対応しているため、Lambdaに設置するものとしてはかなりおすすめです。
https://www.npmjs.com/package/encoding-japanese

UTF32に完全対応しようと思えば、Lambda のサーバー環境と同じ設定でローカル環境をセットアップし、作成した iconv/bin をソースコードの zip に組み込む必要があります。iconv-lite は UTF32, UTF7 に加えて ISO2022-JP1 も動作しませんでした。

まとめ

v4待てる人は待った方がよくない? AWS SDK for JavaScript v3対応を迫られた開発者の参考になれば幸いです。Node.jsは特に苦労しなかったですが、textlintなど直接プロダクトの不具合を引き起こしにくいライブラリについては情報や検証が不足していた感触もあります。着手が遅れると問題発覚も遅くなるので、手早く移行するのが良いと思います。