♠️

AWS SDK for JavaScript v2からv3への移行をなるべく楽に・確実に行う

2024/02/20に公開

現在、私達のプロダクトでは、本番環境のワークロードで100近いLambda関数が動いており、その大半のランタイムがNode.jsで動作しています。

これらは、開発時期によって、構成管理の手段がSAMだったり、CDKだったり、IaC化されていなかったりとバラバラなのですが、いずれにしても、ランタイムのバージョンアップや古くなったライブラリの更新などのモダナイズは、避けて通れません。そのため、できる限り、安全かつ効率的に行いたいですよね。

今回は、Lambda関数コードの内部で使用しているAWS SDKのバージョンをv2からv3に上げました。
ツールの力を使って、なるべく楽に・簡単に移行を行うための備忘メモです。

はじめに

今回の結論としては、ざっくり以下の4点になります。

  • aws-sdk-js-codemodを使うと、簡単に、v2のコードをv3のコードに書き換えてくれる。
  • v3のコードにすることで、ユニットテストでAWS SDK v3 Client mockを使い、テストコードも書きやすくなる。
  • 自動変換やユニットテスト(モック)だけに過信せずに、実環境でのテストは行いましょう。
  • v3の恩恵を最大限受けるためには、足場を固めて、地道な書き換え作業はやっぱり必要。

今回の対象

今回のLambda関数は、以下のAWSリソースに対して、アクセスを行っていました。

  • S3
  • DynamoDB
  • OpenSearch

S3, DynamoDBへのアクセスはAWS SDK v2を使っており、OpenSearchへのアクセスは、aws4による署名バージョン4と、axiosでのHTTPリクエストをしています。

移行の背景ですが、習慣的に実施しているライブラリのアップデートの一環で、axiosのバージョンアップをした際に、署名バージョン4でエラーが出ることになったので、AWS SDKも含めてライブラリを一斉に刷新することにしました。
本来はエラーの原因を究明するところからですが、古いバージョンのライブラリを使い続ける理由は薄いですし、古いバージョンでの原因調査に時間をかけるよりは、現時点のAWSの公式ドキュメントに紹介されている素直な実装にしていったほうが合理的かなと判断しました。そうした経緯から、axiosについても、opensearch-jsに切り替えています。

なお、ライブラリやツールのバージョンアップは、一気にまとめて行うか、個別に行うかは、ケースバイケースで判断することにしています。今回はまとめて行いましたが、サイズや複雑さがそこまで大きくない関数だった、という理由です。

v2はそう遠くないうちにサポートが終了する

以下の通り、v2を使っている場合は、早めにv3に移行すべき段階に来ていると言えるでしょう。

https://github.com/aws/aws-sdk-js

We are formalizing our plans to make the Maintenance Announcement (Phase 2) for AWS SDK for JavaScript v2 in early 2024.

ここでのメンテナンスアナウンス(フェーズ2)というのは、メンテナンスポリシーによると、重大なバグ修正やセキュリティ対応のみだけを行うメンテナンスモード(フェーズ3)の1つ手前の段階になります。

プロダクトコード移行編

AWSのドキュメントやSDKのGitHubリポジトリにも記載がありますが、aws-sdk-js-codemodというツールを使って移行する方法が紹介されています。今回はこのツールを使ってみました。

aws-sdk-js-codemod

公式どおりですが、シンプルな使い方のnpmライブラリです。

$ npm install aws-sdk-js-codemod
$ npx aws-sdk-js-codemod -t v2-to-v3 PATH...

コード側は上記でとりあえずは変換が完了するので、確認や修正を進めるために、package.json, node_modulesを整えていきます。
v3では、これまで一塊になっていたライブラリがモジュール化されており分かれているため、変換されたコードの先頭部分を見ながら、必要なものをインストールします。

$ npm install --save @aws-sdk/lib-dynamodb @aws-sdk/client-dynamodb @aws-sdk/lib-storage @aws-sdk/client-s3

最後に、不要になった古いライブラリをアンインストールします。

$ npm uninstall --save aws-sdk aws4

変換してみた

変換前後のコードは以下のようになります(注:説明用のサンプルです)

v2
const AWS = require('aws-sdk');

const main = async () => {
  // DynamoDB
  const docClient = new AWS.DynamoDB.DocumentClient();
  const { Item } = await docClient
    .get({
      TableName: 'table1',
      Key: {
        key1: 'k1'
      }
    })
    .promise();

  // S3
  const s3 = new AWS.S3();
  const bucketName = '';
  const uploadResult = await s3
    .upload({
      Bucket: 'bucket.123456789',
      Key: 'foo/hoge.csv',
      Body: Buffer.from('Hello')
    })
    .promise();

  const data = await s3
    .getObject({
      Bucket: 'bucket.123456789',
      Key: 'foo/hoge.csv'
    })
    .promise();
  const bodyStrings = data.Body.toString('utf-8').split('\n');
};
v3
const { DynamoDBDocument } = require('@aws-sdk/lib-dynamodb');
const { DynamoDB } = require('@aws-sdk/client-dynamodb');
const { Upload } = require('@aws-sdk/lib-storage');
const { S3 } = require('@aws-sdk/client-s3');

const main = async () => {
  // DynamoDB
  const docClient = DynamoDBDocument.from(new DynamoDB());
  const { Item } = await docClient
    .get({
      TableName: 'table1',
      Key: {
        key1: 'k1'
      }
    });

  // S3
  const s3 = new S3();
  const bucketName = '';
  const uploadResult = await new Upload({
    client: s3,

    params: {
        Bucket: 'bucket.123456789',
        Key: 'foo/hoge.csv',
        Body: Buffer.from('Hello')
      }
  })
    .done();

  const data = await s3
    .getObject({
      Bucket: 'bucket.123456789',
      Key: 'foo/hoge.csv'
    });
  const bodyStrings = data.Body.toString('utf-8').split('\n');
};

v3ではどんなモジュールをどう使うべきか、というのをざっくりつかむ意味でも、こうした変換ツールは頼りになるかと思いました。lib-storage等は、v3で唐突に登場していますし。

変換ルールの完全版は、以下にまとまっています。

https://github.com/aws/aws-sdk-js-codemod/blob/main/TRANSFORMATIONS.md

一部、手作業で修正が必要だった箇所

変換されたコードは概ねそのまま動いたのですが、一部、修正が必要な箇所がありました。

S3.getObjectのBodyですが、 StreamingBlobPayloadOutputTypes というストリームに変わったており、Body.toString は使えなくなっていました。
この部分は transformToString を使うよう、直接修正しました。

const bodyStrings = (await data.Body.transformToString('utf-8')).split('\n');

なお、文字列ではなくバイト列に変換する場合は transformToByteArray を使えばOKです。

動かしてみないと分からないような「ハマり」は、やっぱり存在するので、きちんと動作確認することが大事ですね(特にJavaScriptの場合)

テスト編

AWS SDK v3の特徴としては、TypeScriptの充実したサポート、ミドルウェアスタック、必要なパッケージのみの分割インポート(モジュール化)などが大きいですが、以下で紹介するテストライブラリを使うことで、テストコードを書く生産性が上がるのもメリットかなと感じました。

AWS SDK Client Mock

AWS謹製というわけではないものの、公式のチームからも紹介されており、使い心地のよいモックライブラリです。

https://aws.amazon.com/blogs/developer/mocking-modular-aws-sdk-for-javascript-v3-in-unit-tests/

テストフレームワークはJestを使っていることもあって、v2の時は主にJestのモック機能でテストコードを書いてきていました。
実際、テストコードを書く際に、AWSのアクセス部分のモックに苦労した経験はないでしょうか?
プロダクトコードの書き方によるのかもしれませんが、AWS SDKは書き方の選択肢が1つではないためか、異なる書き方によってモックの使い方もうまく調整しなければならず、私は煩雑さを感じていました("ConfigError: Missing region in config" が出たり…)

こちらのモックライブラリを使うと、以下のような感じでテストを書くことができます。他のテストも同じような書き方で書けそうな気がしますね。

const { S3, PutObjectCommand } = require('@aws-sdk/client-s3');
const { DynamoDBDocument, GetCommand } = require('@aws-sdk/lib-dynamodb');
const { mockClient } = require('aws-sdk-client-mock');

const { handler } = require('./handler');

const s3Mock = mockClient(S3);
const docClientMock = mockClient(DynamoDBDocument);

beforeAll(() => {
  s3Mock.reset();
  docClientMock.reset();
});

describe('handler', () => {
  it('正常系', async () => {
    // テスト対象実行時に呼ばれるモックの振る舞いを定義
    s3Mock.on(PutObjectCommand).resolves({ ETag: '1' });
    docClientMock.on(GetCommand).resolves({
      Item: {
        foo: '123'
      },
    });

    // テスト対象を呼び出す
    await handler(...);

    // S3.Uploadでアップロードされた内容は、以下のような感じで取り出せる
    const callsOfUpload = s3Mock.commandCalls(PutObjectCommand);
    const uploadParams = callsOfUpload[0].args[0].input;
    const uploadContent = JSON.parse(uploadParams.Body.toString());
    expect(uploadContent).toMatchObject({ ... });
  });
});

詳しい使い方などは上記のAWSブログやGitHubリポジトリを参照してください。

https://github.com/m-radzikowski/aws-sdk-client-mock

今後に向けて① TypeScript化

最低限の移行はできたのですが、今後に向けて対応したいと思ったものがあります。

まず1つはコードのTypeScript化ですね。
今回はコードはJavaScriptですが、機会を見つけてTypeScript化はしたいなと痛感しました。

CDKとの親和性も高いですし、ライブラリのアップデートの際に、形式的なチェックが型によって担保されるのは、開発体験上も非常に大きなアドバンテージかなと思います。

aws-sdk-js-codemodはTypeScriptも対応しているので、先にv2のコードをTypeScript化してから、v3に変換するのもよいですが、v2のコードのTypeScript化はv3に変換後は無駄になるものも多いので、先にv3に移行したことは正解だったかなとは思っています。

今後に向けて② v3のメリットを活かした書き方にする

aws-sdk-js-codemod を使った変換は、元々v3がサポートしていたv2と互換性のある書き方での形式的な変換であり、v3のドキュメントで紹介されているクライアントを使った書き方にはなっていないようです。

とりあえず移行するだけであれば、それでも問題はないと思っていますが、ドキュメントと違う書き方になっているは何かと不便ですし、クライアントを使った書き方に、順次切り替えていくのがよいなと思っています。

おわりに

というわけで、aws-sdk-js-codemodを使ったAWS SDK v2からv3への移行方法と、その際に発生した手直し箇所の紹介、および、AWS SDK Client Mockを使ったユニットテストについてでした。

こういったモダナイズ作業は、日々の開発の中で、1年に1回の一大イベントではなく、日常の当たり前タスクとして組み込み、安定したプロダクトを提供していきたいと思います。

Cariot開発チーム

Discussion