AWS SDK v3 のモジュールと利用方法
Tuesday, April 18, 2023 12:21:00 PM
このところ何度か aws-sdk v3 について記事を書いてきましたが、こちらは現時点でのベストプラクティスというか、追記を含むまとめ記事になります。
ランタイムに含まれないモジュールがある?
aws-sdk を使って Lambda から Lambda を実行するときのコードは、公式のExampleコードを見ると以下のようになっています。
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import { InvokeCommand, LambdaClient, LogType } from "@aws-sdk/client-lambda";
import { createClientForDefaultRegion } from "../../libs/utils/util-aws-sdk.js";
/** snippet-start:[javascript.v3.lambda.actions.Invoke] */
const invoke = async (funcName, payload) => {
const client = createClientForDefaultRegion(LambdaClient);
const command = new InvokeCommand({
FunctionName: funcName,
Payload: JSON.stringify(payload),
LogType: LogType.Tail,
});
const { Payload, LogResult } = await client.send(command);
const result = Buffer.from(Payload).toString();
const logs = Buffer.from(LogResult, "base64").toString();
return { logs, result };
};
/** snippet-end:[javascript.v3.lambda.actions.Invoke] */
export { invoke };
ここで注目して欲しいのは
Payload: JSON.stringify(payload),
の部分なのですが、これは TypeScript で記述すると型違反でエラーになります。
/**
* <p>The JSON that you want to provide to your Lambda function as input.</p>
* <p>You can enter the JSON directly. For example, <code>--payload '\{ "key": "value" \}'</code>. You can also
* specify a file path. For example, <code>--payload file://payload.json</code>.</p>
*/
Payload?: Uint8Array;
公式 Example のコードの意味とは… というところですが、
で、そういう issue やら SlackOverflow があって
https://github.com/aws/aws-sdk-js-v3/issues/4623
import { InvocationType, InvokeCommand, LambdaClient } from "@aws-sdk/client-lambda";
import { toUint8Array } from "@aws-sdk/util-utf8";
const lambda = new LambdaClient({});
const response = await lambda.send(
new InvokeCommand({
FunctionName: process.env.LAMBDA_ARN as string,
InvocationType: InvocationType.RequestResponse,
Payload: toUint8Array(payload),
}),
);
こんな感じで Uint8Array
に変換する必要があります。
そこで @aws-sdk/util-utf8
を使ったのですが、これが Node.js v18 の Lambda インスタンスだと見つからないとエラーになります。
執筆時点での Lambda インスタンスの sdk バージョンは 3.188.0
以下のページから現在のランタイムに入っている sdk バージョンがわかります。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtimes.html
執筆時点では 3.188.0 なので、 GitHub リポジトリでそのタグのコードを見てみると、確かにそんなパッケージはありません。
それどころか、 @aws-sdk/util-utf8-node
と @aws-sdk/util-utf8-browser
という2つのパッケージに分かれていました。
どうも、3.300.0 あたりでパッケージを統合したようです。
NPMのレジストリをみると、
https://www.npmjs.com/package/@aws-sdk/util-utf8-node
Deprecated package
This internal package is deprecated in favor of @aws-sdk/util-utf8.
のように書いてあります。
ちなみに、最新の GitHub リポジトリからは、 util-utf8-node
が完全に削除されています。
状況を整理し、これから発生することを整理
つまり、 3.188.0 で @aws-sdk/util-utf8-node
を使っていて、Lambda の zip に sdk を含めていない人は、ランタイムのアップデートで突然 Lambda が動かなくなる可能性があるということです。
これはセマンティックバージョニングとしては破壊的変更なのでメジャーバージョンアップ相当ですが、そもそも aws-sdk がセマンティックバージョニングされているかどうかはわかりません(たぶんされていない)。
僕は長い間、公式ドキュメント「.zip ファイルアーカイブで Node.js Lambda 関数をデプロイする」に書いてある
関数が標準ライブラリまたは AWS SDK ライブラリにのみ依存する場合は、これらのライブラリを .zip ファイルに含める必要はありません
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-package.html
というポリシーを信じていました。 著名な Serverless Framework でもデフォルトの挙動では zip ファイルを作成するときに aws-sdk を除外するようになってます。
道を間違える前に AWS さんに確認してみた
僕の懸念は
- aws-sdk は zip に含めるべきか?
- 後方互換性がなくなる変更があるけど、アップデートの方針はどうなっているか?
というあたりです。
aws-sdk は zip に含めるべきか?
こちらは「はい」が正解ということです。
AWS Lambda 関数を使用するためのベストプラクティス に 関数のデプロイパッケージ内で依存関係を制御します
という部分があり、zip に含めた方が良いとなっています。
しかし aws-sdk のサイズはモジュール化されているとはいえ、Lambdaの上限サイズを考えるとかなりの割合を占めてしまうので、できればランタイムを使いたいとことです。
後方互換性がなくなる変更があるけど、アップデートの方針はどうなっているか?
こちらの回答をふりかえる前に、aws-sdk v3 のモジュール構造について補足しておきます。 GitHub のリポジトリにある README Generated Code] によると以下のとおりです。ざっくり要約しました。
v3 コードベースは、AWSサービスが公開しているモデルから生成されています。 smithy-typescript で
/clients
サブディレクトリ内のコードを生成し、これらのパッケージは @aws-sdk/client-XXXX のような名前になります。クライアントは、
/packages
にあるユーティリティコードに依存します。これらのコードは手動で記述されていて、通常あまり役に立ちません。
/lib
には高レベルのライブラリがあります。 client をラップして操作しやすくするライブラリです。よくある例は@aws-sdk/lib-dynamodb
で Amazon DynamoDB のアイテムの操作を簡素化するものや、@aws-sdk/lib-storage
で S3 の multipartUpload での並列アップロードを簡素化するものです。
続けて、以下のようにも書いてあります。
- /packages 手動でコード更新が行われる場所で、 NPM に @aws-sdk/XXXX で公開されています。特別なプレフィックスはありません。
- /clients このディレクトリのコードは自動生成され、 /packages に依存します。AWS のサービスと 1 対 1 です。通常、手動編集はここでは行わないでください。@aws-sdk/client-XXXX で NPM に公開されます。
- /lib このディレクトリは、 /clients に依存します。既存の AWS サービスと操作をラップして、Javascript での作業を容易にします。@aws-sdk/lib-XXXX で NPM に公開されています。
上記以外にも private というのもあったりしますが、これは名前からも明らかに非公開のモジュールだとわかります。 NPM 公開されていて、僕らが利用することができるものが client / packages / lib にあるといった感じでしょうか。
上記3つのディレクトリについての AWS アップデートポリシーは以下のようなものになるそうです。
- client ユーザーが利用する想定のモジュールであり、破壊的変更は行われない
- lib ユーザーが利用する想定のモジュールであり、破壊的変更は行われない
- package client からの利用を想定している内部モジュールなので破壊的変更の可能性がある(ただしドキュメントに明示されていない)
package に入っているもので、README に利用方法が解説されているもの、たとえば S3 の署名付きURLを発行するためのライブラリ @aws-sdk/s3-request-presigner はユーザーからの利用が想定されていて、破壊的変更は行われないようです。 しかし、README にAPIの解説がないものについては内部利用を想定しているため、破壊的変更もありえると。
後者は private ディレクトリに入れた方が良いのでは?と思わなくはないですが、何か理由があるんでしょうかね。
今回のようにドキュメントに記述がない @aws-sdk/util-utf8-node
を使いたい場合は zip に必ず含むようにしましょう。
それ以外は zip の容量が厳しければ v2 のときと同じようにランタイムに依存する方が良いと思います。aws-sdk の更新頻度がかなり多いので、どれがセキュリティパッチかわからないし、できればランタイムでの更新に期待したいという思いもあります。
さいごに
これまでの aws-sdk v3 / Node.js v18 への移行記事を一覧にしてみます。
- AWS Lambda の Node.js 14 を 18 に移行する(CI/CD環境移行編)
- AWS Lambda の Node.js 14 を 18 に移行する(aws-sdk v3 移行編)
- aws-sdk v3 で TS2345 が出てコンパイルエラーになる
- aws-sdk v3 でコンパイルエラーになる - その2
- Node.js v18 / aws-sdk v3 の Lambda アプリが突然動かなくなる
- aws-sdk v3 を使うライブラリを作ったときは、なるべく peerDependencies に設定しよう
- 本記事
私の思う現時点でのベストプラクティス
- CIのイメージは公式の Lambda Docker イメージが便利
- aws-sdk v3 のユニットテストには aws-sdk-client-mock が良い
- aws-sdk の各モジュールのバージョンは揃えて、なるべく最新を使おう
- aws-sdk 関数/クラス以外、たとえば enum の値などは基本的に利用しない
- peerDependencies に書いてあるモジュールもバージョンを揃えてインストールしよう
- packages にあるドキュメントなしのモジュールを使う場合は、zip に含める
- zip の容量に余裕があれば aws-sdk はすべて含めた方が良い
現時点はかなりクセがあるというか、ノウハウが必要であるというのが現実だと思います。 とくに AWS Lambda + aws-sdk v3 + TypeScript の場合にはですね。
ぶっちゃけ TypeScript でなければコンパイルエラーや型違反についての問題もないし、しいていえば zip に含めるかどうか?ぐらいです。 とはいえ、みんなもう Node.js ランタイム選ぶなら TypeScript 使うだろうと思うので、これまでの記事のノウハウが役に立てば幸いです。
ts-jest が esbuild/swc をトランスフォーマーに使って高速化していた
Thursday, April 13, 2023 02:54:00 PM
昨年 @swc-node/jest を使ってテストを高速化する という記事を書きました。
その時点で ts-jest でのテストが遅くて、 @swc-node/jest
に切り替えていました。
その後 @swc-node/jest
もなんやかんやあって、たまに動かなくなったりして issue 投稿して直してもらったりいろいろあったのですが、最近 ts-jest の状況を見てみたら、こんな記述がありました。
Starting from v28.0.0, ts-jest will gradually opt in adoption of esbuild/swc more to improve the performance. To make the transition smoothly, we introduce legacy presets as a fallback when the new codes don’t work yet.
from https://kulshekhar.github.io/ts-jest/docs/getting-started/presets
なんと、高速化のために esbuild/swc を使うようになったって。まじかー
早速 ts-jest に変えてみた
@swc-node/jest
から ts-jest
に変更して、 jest.config.js
を preset
に変更。
テストを実行してみると、確かに速くなってる!
手元のプロジェクトだと、 ts-jest
にかかる時間は、 @swc-node/jest
+ tsc
の時間とほぼ一致していました。
ts-jest
ではコンパイルエラーも検知されるので、つまりそういうことでしょう。
また ts-jest で大丈夫!
ということで、 jest の実行に swc や esbuild を使うことや、tsconfig と違う設定を考慮したりとかなく、TypeScript のコードを jest でテストできるようになりました。
私たちのプロジェクトは、早速すべて ts-jest@29 に切り替えました。 もし問題がある場合は、 Preset にレガシーモードを指定すると esbuild や swc を使わないようにできるようですが、そもそも一度 swc に切り替えてテストできていれば、問題なく ts-jest に戻れるはずです。
今後は TypeScript + jest での開発環境に ts-jest のご利用をお勧めします。
aws-sdk v3 を使うライブラリを作ったときは、なるべく peerDependencies に設定しよう
Tuesday, April 11, 2023 04:33:00 PM
先日 aws-sdk v2 が 2023 年中にメンテナンスモードになる という記事を書いて、実際自分たちで作っていた aws-sdk v2 でできていた CLI ツールも aws-sdk v3 に切り替えました。
CLIツールを v3 に移行したら、利用するアプリ側がコンパイルエラーになる
で、それを Lambda にデプロイするアプリの devDependencies に反映してみたところ、また型エラーが出るようになりました。
CLIツールでは @aws-sdk/client-lambda@3.310.0
に依存していて、 Lambda アプリ側は @aws-sdk/client-lambda@3.254.0
を使っていたためです。
error TS2345: Argument of type ‘typeof LambdaClient’ is not assignable to parameter of type ‘InstanceOrClassType
>’. Type ‘typeof LambdaClient’ is not assignable to type ‘ClassType >’.
で、それぞれの型定義への依存は "@aws-sdk/types": "*"
になっているので、lock ファイルの状態次第でそれぞれでバージョンが異なるものが入る場合があります。
そこで型が一致しない問題が発生してきます。
CLI ツールでは実行時に aws-sdk が必要になるので、 dependencies に設定していて、CLIツールを使う方のアプリは devDependencies に設定しているわけですが、それぞれの dependencies からインストールされる @aws-sdk/types
がドッチ?問題になってしまうという…
解決策
今回は自分たちで作ったライブラリだったので、CLIツール側の aws-sdk を devDependencies に変更して、CLIツールを使うアプリ側の aws-sdk 利用状況に依存するように変更しました。
"peerDependencies": {
"@aws-sdk/client-lambda": "^3.0.0"
},
これでアプリ側が @aws-sdk/client-lambda
を使っていればそのバージョンに依存するようになるし、使っていなければ 3系の最新が入るようになります。
ここで、この aws-sdk v3 シリーズを読んでいただいた懸命な皆様はお気づきだと思いますが Node.js v18 / aws-sdk v3 の Lambda アプリが突然動かなくなる でも書いた
peerDependencies
が指定されていたら、バージョンを揃えるために自分でインストールする
これが重要になってきます。 なので、自分で作ったライブラリで peerDependencies に指定したら、結局アプリ側で使ってなくてもバージョンを揃えるのに追加でインストールした方が良いということです。
aws-sdk v3 を安心して使うためには(追記)
- 基本的にバージョンは最新版、もしくは最新に近い同じバージョンに揃える
peerDependencies
が指定されていたら、バージョンを揃えるために自分でインストールする- 関数/クラス以外、たとえば enum の値などは基本的に利用しない
- 自作ライブラリで aws-sdk v3 に依存する場合は
peerDependencies
に指定する
大事なこと4つ目を追加しました。
aws-sdk@v3 のパッケージ管理に日々不安が募りますが、同様の問題に遭遇した人の解決に役立てれば幸いです。
aws-sdk v2 が 2023 年中にメンテナンスモードになる
Thursday, April 06, 2023 06:24:00 PM
aws-sdk v2 を使っている CLI ツールで aws 環境を操作していたら、見慣れないメッセージが出てきました。
(node:9129) NOTE: The AWS SDK for JavaScript (v2) will be put into maintenance mode in 2023.
AWS SDK v2 ってメンテナンスモードに入るんだ…
Twitter でも書いている人がいました
Anyone else seeing this new error from @awscloud sdk?
— David Wells (@DavidWells) February 16, 2023
This is showing to our CLI users and is… annoying to say the least. pic.twitter.com/KabEtFDKnl
これまでも複数回にわたって aws-sdk v3 の記事を書いてきましたが、これは Lambda にデプロイするアプリだから v3 に移行したのであって、CLIツールとかなら v3 に移行するモチベがあるかな….
ぶっちゃけ Lambda とかだとランタイムに追従すると sdk も上げないといけないので対応したけど、ツールなら対応しないですよね。
公式のリポジトリにも、サポートについて書かれています。
https://github.com/aws/aws-sdk-js#version-2x-support
そこにリンクされている メンテナンスポリシーページ にいくと、以下のような内容が記入されていました。
メンテナンス (フェーズ 3) -メンテナンス モードの間、AWS は SDK リリースを制限して、重大なバグ修正とセキュリティ問題のみに対処します。SDK は、新規または既存のサービスの API 更新を受け取ることも、新しいリージョンをサポートするために更新されることもありません。特に指定がない限り、メンテナンス モードのデフォルト期間は 12 か月です。
サポート終了 (フェーズ 4) - SDK がサポート終了に達すると、更新またはリリースを受け取ることができなくなります。以前に公開されたリリースは引き続きパブリック パッケージ マネージャーから入手でき、コードは GitHub に残ります。GitHub リポジトリはアーカイブされる場合があります。サポートが終了した SDK の使用は、ユーザーの判断で行われます。ユーザーが新しいメジャー バージョンにアップグレードすることをお勧めします。
v2 自体がリポジトリから無くなることはなさそうですが、膨大にありそうな v2 を使った CLI ツール群はどうするのかなぁ…
ちなみに Serverless Framerwork は PR Suppress AWS SDK v2 deprecation message でいったんメッセージ出力を OFF にしていますが、 v3 移行は大変だろうなぁ、という感想しかないです。
AWS SDK v3 は、これまでの記事でも書いてますが、結構パッケージ管理が残念な感じだったりするのと、書き方もめっちゃ変わっているので Serverless Framework 規模になると書き換えもかなり工数かかりそうだなぁと(いつかはやるんだろうけど)。
AWS さんが簡易にコンバートできるツールを作ってくれると良いんだろうけど、オプションの書き方も変わっているところがあったりするので、単純な変換も大変そうですね。
上記の引用ツイートのレスの中にもあるけど、v3 のドキュメントが不親切だというのは事実だし、良い解決策が 2023 年中に見つかると良いですね。 なんか感想ブログになっちゃいましたが、僕らのプロジェクトも v2 依存している CLI ツールは移行対象にしてなかったので、これから移行計画作らないとな、と思っています。
Node.js v18 / aws-sdk v3 の Lambda アプリが突然動かなくなる
Wednesday, April 05, 2023 05:08:00 PM
「Lambda アプリが突然動かなくなる」なんて、どうせバグなんでしょーというのが当然のリアクションですが、これは本当にバグなのか…
本当にあった怖い話をします。
ある日、Lambda アプリが突然動かなくなる
Node.js v18 のライタイムで動く Lambda にデプロイされているアプリで、ある日突然エラーが出るようになりました。
original: TypeError: Cannot read properties of undefined (reading ‘RUNNING’)
この部分ですが TypeScript のコードでは以下のようになっていました。
import {
DescribeExecutionCommand,
ExecutionDoesNotExist,
ExecutionStatus,
SFNClient,
StartExecutionCommand,
paginateListExecutions,
} from '@aws-sdk/client-sfn';
// 省略
if (latest?.status === ExecutionStatus.RUNNING) {
ExecutionStatus
は StepFunctions のクライアントで export されている値です。
StepFuntions のジョブが実行中かどうか判定している箇所で。少し前まで普通に動いていたのに….
Lambda アプリの構成
AWS Lambda は aws-sdk がランタイムにグローバルインストールされているので、最新版に追従して問題なければパッケージに含める必要はありません。 Lambda にデプロイするアプリケーションのサイズを少しでも減らすために、 aws-sdk は入れないようにしています。
node コンソールで確認してみる
手元では devDependencies に @aws-sdk/client-sfn
が入っているので、 node コンソールに入って require を実行してみました。
> const { ExecutionStatus} = require('@aws-sdk/client-sfn')
undefined
> ExecutionStatus
{
ABORTED: 'ABORTED',
FAILED: 'FAILED',
RUNNING: 'RUNNING',
SUCCEEDED: 'SUCCEEDED',
TIMED_OUT: 'TIMED_OUT'
}
ふむ、問題なさそうだが?
最新版の aws-sdk で変わったのだろうか? 一時的に最新版にバージョンアップをしてみて、確認してみます
> const { ExecutionStatus} = require('@aws-sdk/client-sfn')
undefined
> ExecutionStatus
undefined
えええええええーーー
aws-sdk の該当箇所の修正履歴をチェックする
hore(codegen): export enums as const
元のコード
/**
* @public
*/
export enum ExecutionStatus {
ABORTED = "ABORTED",
FAILED = "FAILED",
RUNNING = "RUNNING",
SUCCEEDED = "SUCCEEDED",
TIMED_OUT = "TIMED_OUT",
}
変更後のコード
/**
* @public
* @enum
*/
export const ExecutionStatus = {
ABORTED: "ABORTED",
FAILED: "FAILED",
RUNNING: "RUNNING",
SUCCEEDED: "SUCCEEDED",
TIMED_OUT: "TIMED_OUT",
} as const;
おいおい、これって @public
なのに後方互換性なくなっとるだろ….
なぜ undefined になるのか
enum はコンパイルされて JavaScript になると以下のようなコードになります。
var ExecutionStatus;
(function (ExecutionStatus) {
ExecutionStatus["ABORTED"] = "ABORTED";
ExecutionStatus["FAILED"] = "FAILED";
ExecutionStatus["RUNNING"] = "RUNNING";
ExecutionStatus["SUCCEEDED"] = "SUCCEEDED";
ExecutionStatus["TIMED_OUT"] = "TIMED_OUT";
})(ExecutionStatus = exports.ExecutionStatus || (exports.ExecutionStatus = {}));
でも export const as const
になると、JavaScript にはコンパイルされることなく、変数の利用箇所に埋め込みの形になります。
最初にも書きましたが、 aws-sdk v2 では普通だった
AWS Lambda は aws-sdk がランタイムにグローバルインストールされているので、最新版に追従して問題なければパッケージに含める必要はありません。 Lambda にデプロイするアプリケーションのサイズを少しでも減らすために、 aws-sdk は入れないようにしています。
こういう運用はすでにオワコンなんでしょうか? いやでも、v3 になってもランタイムに aws-sdk は入っています。
少なくとも ExecutionStatus
が内部変数で export されてない private な値だったら良いのですが、これはさすがにマズイんじゃないですかね?awsさん。
この問題が発生するケース
- AWS Lambda でランタイムの aws-sdk を使っていて、TypeScript から enum の値を参照していた場合(同一の変更ですべての enum が const に書き変わっています)
amplify とか Lambda でなくコンパイルされているケースでは問題は起きませんが、いやこういうのサクッと変更しちゃダメだと思うんですが… しかもランタイム側に入っているので、動作している Lambda アプリケーションが突然動作しなくなります。
aws-sdk v3 を安心して使うためには(追記)
- 基本的にバージョンは最新版、もしくは最新に近い同じバージョンに揃える
peerDependencies
が指定されていたら、バージョンを揃えるために自分でインストールする- 関数/クラス以外、たとえば enum の値などは基本的に利用しない
大事なこと3つ目を追加しました。
aws-sdk@v3 のパッケージ管理に日々不安が募りますが、同様の問題に遭遇した人の解決に役立てれば幸いです。
Recent Articles
- GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 2 2024/03/20
- GAE gen1 で動いている PHP5.5 で作った個人開発サービスを gen2 PHP8.2 へ移行した1年記 〜 その 1 2024/03/20
- マルチプルレポをモノレポへコミットログを残しながら移行する 2023/09/27
- tsyringe を TypeScript 5 で使う方法 2023/05/02
- LocalStack を使って aws-sdk の Integration Test を実行する 2023/04/19
- AWS SDK v3 のモジュールと利用方法 2023/04/18
- ts-jest が esbuild/swc をトランスフォーマーに使って高速化していた 2023/04/13
- aws-sdk v3 を使うライブラリを作ったときは、なるべく peerDependencies に設定しよう 2023/04/11
- aws-sdk v2 が 2023 年中にメンテナンスモードになる 2023/04/06
- Node.js v18 / aws-sdk v3 の Lambda アプリが突然動かなくなる 2023/04/05