aws-sdk v3 でコンパイルエラーになる - その2
Tuesday, April 04, 2023 04:20:00 PM
先日の aws-sdk v3 で TS2345 が出てコンパイルエラーになる という記事でも書いたとおり、 @aws-sdk/client-xxxxx
を追加するときは
新しく
@aws-sdk/client-xxxx
を追加するときは、既存のクライアントも含めてすべて同じバージョンに変更する
のがオススメなのは変わりがないのですが、これだけだとうまく対応できないことがあったので、別記事として書いておきます。
ある日の変更前依存パッケージ
"devDependencies": {
"@aws-sdk/client-dynamodb": "^3.264.0",
"@aws-sdk/client-s3": "^3.264.0",
"@aws-sdk/client-s3-control": "^3.264.0",
"@aws-sdk/client-secrets-manager": "^3.264.0",
"@aws-sdk/lib-dynamodb": "^3.264.0",
}
DynamoDB, S3, Secret Manager を利用するようになっていました。
パッケージを追加したらエラーになる
で、そこに @aws-sdk/lib-storage
というパッケージを追加しようとしたところ、また先日と同様に TS2345
でコンパイルエラーになります。
ちゃんとバージョンをすべて最新にしてみたのですが、ダメでした。
ふたたび issue を探る
そうすると、issue ではなく discussions に変更されてしまったものを発見しました。
Peer dependencies not pinned for lib-dynamodb
なぜこんなことになるのか
やはりパッケージ管理が崩壊しているとしか思えないのですが、何でこうなっているのかというのを解説していきます。
以下は、今回追加しようとした @aws-sdk/lib-storage
の package.json
の一部です。
追加時点の lib-storage
のバージョンが 3.272.0
だったので、上記の変更前依存パッケージでインストール済みだったものも 3.272.0
に更新済みです。
"dependencies": {
"@aws-sdk/middleware-endpoint": "3.272.0",
"@aws-sdk/smithy-client": "3.272.0",
"buffer": "5.6.0",
"events": "3.3.0",
"stream-browserify": "3.0.0",
"tslib": "^2.3.1"
},
"peerDependencies": {
"@aws-sdk/abort-controller": "^3.0.0",
"@aws-sdk/client-s3": "^3.0.0"
},
peerDependencies
の依存がありますね。
で、すでにインストール済みだった @aws-sdk/client-dymanodb
の package.json
の一部も見てみましょう。
"dependencies": {
"@aws-crypto/sha256-browser": "3.0.0",
// 省略
"@aws-sdk/smithy-client": "3.272.0",
"@aws-sdk/types": "3.272.0",
// 省略
"uuid": "^8.3.2"
},
依存が多いのでちょっと省略しましたが、注目したいところだけ書きました。
こちらには peerDependencies
の依存はありません。
@aws-sdk/client-s3
は上記のとおりインストール済みだったので、 @aws-sdk/abort-controller
を見てみましょう。
"dependencies": {
"@aws-sdk/types": "3.289.0",
"tslib": "^2.3.1"
},
おや? @aws-sdk/types
のバージョンが違うじゃん…
“@aws-sdk/abort-controller”: “^3.0.0”,
なぜ、ここは ^3.0.0
なのだ??
というのが、最初のディスカッションでもふれられている訳ですが、本当にパッケージ管理が (ry
対応案
使っている aws-sdk のパッケージで peerDependencies
になっているのを調べて、すべてバージョン指定でインストールします。
ということで変更後の依存は以下のとおりにします。
"@aws-sdk/abort-controller": "3.272.0",
"@aws-sdk/client-dynamodb": "3.272.0",
"@aws-sdk/client-s3": "3.272.0",
"@aws-sdk/client-s3-control": "3.272.0",
"@aws-sdk/client-secrets-manager": "3.272.0",
"@aws-sdk/lib-dynamodb": "3.272.0",
"@aws-sdk/lib-storage": "3.272.0",
"@aws-sdk/smithy-client": "3.272.0",
"@aws-sdk/types": "3.272.0",
abort-controller, smithy-client, types が利用しているパッケージの中で peerDependencies
にあったので、すべてバージョンを揃えてコンパイルエラーは解消されました。
aws-sdk v3 を安心して使うためには
- 基本的にバージョンは最新版、もしくは最新に近い同じバージョンに揃える
peerDependencies
が指定されていたら、バージョンを揃えるために自分でインストールする
この2つとても大事です。
こんなの discussions に変更した上で放置していて良いのか?…
とても悲しい状況ですが、同様の問題に遭遇した人の解決に役立てれば幸いです。
aws-sdk v3 で TS2345 が出てコンパイルエラーになる
Monday, April 03, 2023 03:47:00 PM
dependabot で @aws-sdk/client のバージョンアップがコンパイルエラーになる
少し久しぶりの記事になってしまいましたが、この間も Node.js v18 / aws-sdk v3 への移行を行なっています。 一部のリポジトリではコードの移行は終わって、 dependabot でライブラリの最新追従を行なっているのですが、以下のようなコンパイルエラーが出るようになりました。
error TS2345: Argument of type 'typeof LambdaClient' is not assignable to parameter of type 'InstanceOrClassType<Client<ServiceInputTypes, MetadataBearer, any>>'.
Type 'typeof LambdaClient' is not assignable to type 'ClassType<Client<ServiceInputTypes, MetadataBearer, any>>'.
aws-sdk v3 では複数の aws サービスを使っていると複数のパッケージに依存するようになるのですが、複数のバージョンアップが dependabot によって PR されるうち、一部のアプデだけ上記のようなエラーになります。
issue を探してみる
aws-sdk の issue を調べていくと、それっぽいものがありました。
Typescript compilation problems since 3.52.0 in lib-dynamodb
この issue 自体はクローズされていないのですが、コメントのスレッドの中に有用な情報がありました。
@aws-sdk/types
が依存に入っていて、異なるバージョンの client-xxx があるとエラーになる(なんで package-lock.json を消してから npm i しなおすとうまくいくよ -> そんなんやるか!)@aws-sdk/client-xxxx
の各クライアントはすべて最新バージョンに追従してください
のようなものです(要約してあります)。
aws-sdk@v3 はサービスごとにパッケージを分割することで v2 と比べて良い、というのがウリなはずなんですが、これではパッケージ管理崩壊しているのでは?と思わなくもないですが、まぁ各クライアントのバージョンを合わせれば良いだけなので、従うことにしました。
ちなみにこの時点では複数の @aws-sdk/client-xxx
を全て最新版に追従することで解消されました。
この問題が発生する状況
この問題が発生する状況は以下のとおりです。
- すでに1つ以上の
@aws-sdk/client-xxxx
を入れていて、後日機能追加によって別のクライアントをインストールするとき - dependabot でパッケージごとにバージョンアップの PR が発動する
おすすめの対応
なので、現時点でのオススメの対応は以下のとおりです。
- 新しく
@aws-sdk/client-xxxx
を追加するときは、既存のクライアントも含めてすべて同じバージョンに変更する - dependabot による更新を
@aws-sdk/*
については無効にする
Github Actions の dependabot でアプデを無効にするには、 dependabot.yml を以下のように記述すれば良いです。
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
ignore:
- dependency-name: '@aws-sdk/*'
# aws-sdk に対するすべての更新を無視
私たちは AWS Lambda の Node.js v18 ランタイムを使っているので、aws-sdk はランタイムにグローバルインストールされているから、あえて最新版に追従しなくても問題ないというのが主な理由です。 新しいクライアント追加のときに最新に追従すれば十分という判断です。
さいごに
aws-sdk@v3 を使ったアプリのナレッジが少ないので、何か小さなことでも記事にしていこうと思います。 同様の問題に遭遇した人の解決に役立てれば幸いです。
aws-sdk-client-mock はどのように aws-sdk をモックしているのか?
Thursday, February 02, 2023 05:05:00 PM
前回の記事AWS Lambda の Node.js 14 を 18 に移行する(aws-sdk v3 移行編)で
aws-sdk-client-mock
便利ですね。 しかし、どうやって SFNClient をモックしているんだろうか?と思いませんか? sfn.start の実装側では、普通に aws-sdk から import した SFNClient を使って実装していて、mockClient
の結果で得られたモックを使っているわけでもありません。 そのあたりは、次回の記事で明らかにしていきたいと思っていますので、ご期待ください。
書いたとおり、どうやってモックしているのか、紹介していきます。
前回のおさらい
aws-sdk v3 でのテストコードは以下のようになることを紹介しました。
import 'aws-sdk-client-mock-jest';
import { mockClient } from 'aws-sdk-client-mock';
import { SFNClient, StartExecutionCommand } from '@aws-sdk/client-sfn';
const SFNClientMock = mockClient(SFNClient);
beforeEach(() => SFNClientMock.reset());
SFNClientMock.on(StartExecutionCommand).resolves({ executionArn });
const resuls = await sfn.start(sfnArn, event);
expect(results.executionArn).toBe(executionArn);
expect(SFNClientMock).toHaveReceivedNthCommanddWith(1, StartExecutionCommand, {
stateMachineArn: arn, input: JSON.stringify(event)
});
モックするときは mockClient(SFNClient)
とクラスを渡しているだけでしたね。
生成した SFNClientMock
はテストクラスでしか利用していません。
aws-sdk-client-mock の実装を見てみる
mockClientのコードを見てみましょう。
export const mockClient = <TInput extends object, TOutput extends MetadataBearer>(
client: InstanceOrClassType<Client<TInput, TOutput, any>>,
): AwsClientStub<Client<TInput, TOutput, any>> => {
const instance = isClientInstance(client) ? client : client.prototype;
ふむふむ mockClient
の引数がインスタンスかどうか調べて、クラスだったら prototype が利用されるわけですか。
さらに見てみると
const sendStub = stub(instance, 'send') as SinonStub<[Command<TInput, any, TOutput, any, any>], Promise<TOutput>>;
なるほど send
メソッドをスタブしてますね。
あー、つまりクラスを指定すると、 prototype 定義が置き換わっちゃうわけですね。
使い分けが必要
prototype が置き換わることで mockClient
に渡したクラスの send
はすべてのシーンでスタブに置き換わります。
テストしたい実装側でクライアントのインスタンスをDIなどで注入していない場合は、この方法で良いですね。
逆にシーンによっては、実際に send を実行したい場合は困ってしまいます。
localstack などを使って部分的にインテグレーションテストをしたい場合は、なるべくインスタンスをDIできる仕組みにした方が良いでしょう。
インスタンスを生成するメソッドを作って、それをモックして mockClient
の戻り値を返すようにしても良いですね。
とくにイングレーションテストとかなく、すべてのクライアント呼び出しをモックするのであれば、クラス指定が楽ですね。
このように使い分けることで、テストコードを書いていきたいですね。 簡単ではありますが、aws-sdk-client-mock はどのように aws-sdk をモックしているのか? を紹介しました。
AWS Lambda の Node.js 14 を 18 に移行する(aws-sdk v3 移行編)
Wednesday, January 04, 2023 11:00:00 AM
先日も紹介したクラメソさんの [アップデート] AWS Lambdaが Node.js 18をサポートしました 記事でもふれられていましたが、Node.js が 18 になるのに伴い、イメージ内の aws-sdk が v3 になりました。 Node.js 16 までは v2 系だったのですが、ここでは多くの変更が発生しています。
- npm install する対象が
aws-sdk
から@aws-sdk/client-xxx
のように AWS のサービスごとのクライアントライブラリになった - 書き方が、各クライアントを
new
してsend
でコマンドを送信する記述に統一された
大きくはこの2点でしょうか。
私たちは aws-sdk の利用に関してリポジトリパターンの中に閉じ込めているので、基本的にそこだけ対応すれば良いのですが、API で同期実行だったものが非同期実行に変更となっているので、そこだけはインターフェースを Promise に変更する必要があります。
これは実際のコマンド内部は同期で問題ないけど、 Client.send(command)
が基本的に Promise を返却するようになったための副作用だと思ってください。
( aws-sdk v2 のときは command().promise()
のようにすると、非同期コマンドは Promise を返却するようになっていたのですが、この promise()
部分がなくなり、基本的な戻りは Promise になっています)
具体的なコードで見る v2 と v3 の違い
StepFuntions を実行するコードで違いをみてみましょう。
v2 のコード
import { StepFunctions } from 'aws-sdk';
const stepFunctions = new StepFunctions();
const results = await stepFunctions.startExecution({
stateMachineArn: arn,
input: JSON.stringify(event),
}).promise();
v3 のコード
import { SFNClient, StartExecutionCommand } from '@aws-sdk/client-sfn';
import { build, parse } from '@aws-sdk/util-arn-parse';
const { region } = parse(arn);
const stepFunctions = new SFNClient({ region });
const results = await stepFunctions.send(new StartExecutionCommand({
stateMachineArn: arn,
input: JSON.stringify(event),
}));
StepFunctions では、利用するライブラリ名が aws-sdk/client-sfn
になりました。
ほとんどの場合は、サービス名を使っています(たとえば S3 なら aws-sdk/client-s3 だし、 S3Client です)が、StepFunctions の場合は sfn
に省略されているので注意が必要です。(僕はこの略名に気付くまでメチャクチャ探したことを告白しておきます)
クライアントライブラリを探すときは AWS SDK for JavaScript v3 の公式ドキュメントを参照すると良いのですが、ぱっとみてサービス名からクライアントライブラリが見つけられない場合は、それっぽい略名から探してみると良いでしょう。
v2 と v3 を比較するとわかりますが、大きな変更でないことはわかると思います。
v3 移行するときに役立つドキュメント
次に v3 移行するときに役立つドキュメントを紹介していきます。
1つ目は公式のSDKドキュメントですね。クライアントライブラリを探すときも、コマンド名を確認するときにも良く参照します。
2つ目はエラーハンドリングのやり方の違いについてです。詳しくはドキュメントを読めばわかるのですが、従来はエラーの判定方法が
(<AWSError>e).code === 'ExecutionDoesNotExist'
みたいにしていたのを e instanceof ExecutionDoesNotExist
のように例外クラスのインスタンスとして判定できるようになっています。これも v3 に移行するときに注意したいポイントです。
3つ目はSDKのv3へのアップグレードガイドで、主に破壊的変更があった部分について解説されています。単純に XxxxClient と XxxxCommand に変更したときに型エラーが出たときは、このドキュメントを読んで破壊的変更(変数名の変更や、JSON構造の変更など)がないか確認すると解決への近道になります。
ただし、先ほどの例で書いた StepFunctions のコンストラクタのように
const stepFunctions = new SFNClient({ region });
リージョンが v3 から必須パラメータになったのですが、こういった変更は動かしてみるまでわからないことが多いので、ユニットテストを書いておくことは重要です。
ユニットテスト
私たちは以下のサービスについては、モックせずにローカルで代替ライブラリを使って結合テストを行なっています。
- S3 : S3rver
- SQS: ElasticMQ
- RDS: better-sqlite3
v2 でのユニットテスト
上記以外について、v2 のときは以下のようにモックしていました。ここでは Jest での書き方を紹介します。 sfn の start というメソッドで StepFunctions を呼び出していると想定してください。
const startExecutionMock = jest.fn(); // startExecution の引数チェック用
const startExecution = jest.fn(); // startExecution の戻り値モック用
jest.mock('aws-sdk', () => {
return {
StepFunctions: jest.fn().mockImplementation(() => {
return {
startExecution: startExecutionMock.mockImplementation(() => {
return { promise: startExecution };
}),
};
});
};
});
beforeEach(() => {
startExecutionMock.mockReset();
startExecution.mockReset();
});
startExecution.mockResolvedValue({ executionArn });
const resuls = await sfn.start(sfnArn, event);
expect(results.executionArn).toBe(executionArn);
expect(startExecutionMock.mock.calls).toEqual([
[{ stateMachineArn: arn, input: JSON.stringify(event) }]
]);
v2 でのモックは面倒でしたね。
v3 でのユニットテスト
v3 で aws-sdk をモックするときは AWS SDK v3 Client mock を利用します。
Library recommended by the AWS SDK for JavaScript team - see the introductory post on the AWS blog.
ということで公式にもオススメされるライブラリということで良いですね。
上記の v3 でのテストコードは以下のようになります。
import 'aws-sdk-client-mock-jest';
import { mockClient } from 'aws-sdk-client-mock';
import { SFNClient, StartExecutionCommand } from '@aws-sdk/client-sfn';
const SFNClientMock = mockClient(SFNClient);
beforeEach(() => SFNClientMock.reset());
SFNClientMock.on(StartExecutionCommand).resolves({ executionArn });
const resuls = await sfn.start(sfnArn, event);
expect(results.executionArn).toBe(executionArn);
expect(SFNClientMock).toHaveReceivedNthCommanddWith(1, StartExecutionCommand, {
stateMachineArn: arn, input: JSON.stringify(event)
});
モックライブラリを使うと、v2 のときより可読性も良くわかりやすくなっています。
さいごに
ここまで aws-sdk を v2 から v3 にする方法をまとめてみました。
aws-sdk-client-mock
便利ですね。
しかし、どうやって SFNClient をモックしているんだろうか?と思いませんか?
sfn.start の実装側では、普通に aws-sdk から import した SFNClient を使って実装していて、 mockClient
の結果で得られたモックを使っているわけでもありません。
そのあたりは、次回の記事で明らかにしていきたいと思っていますので、ご期待ください。
AWS Lambda の Node.js 14 を 18 に移行する(CI/CD環境移行編)
Friday, December 30, 2022 03:39:00 PM
クラメソさんの [アップデート] AWS Lambdaが Node.js 18をサポートしました 記事のとおり、やっと AWS Lambda でも Node.js 18 が使えるようになりました。
現在 Node.js 14 を使って AWS Lambda で API サーバーを構築しているのですが、 Node.js 16 の LTS が 9ヶ月終了が早まったこともあり、 このタイミングで Node.js 18 へ移行することにしました。
本稿では CI/CD 環境を Node.js 14 から Node.js 18 へ移行するときに実施した作業を振り返って、まとめておきます。
現在の構成はこんな感じです。
- Node.js 14 / TypeScript
- CI/CD に GitHub Actions を利用
- Amazonlinux ベースの amazon/aws-cli コンテナをベースにパッケージを追加した独自コンテナイメージで CI/CD を実施
- Serverless Framework でビルド/デプロイ
CI/CD イメージの Node.js を 18 にする
まずはビルド/デプロイをするコンテナイメージを Node.js 18 にしていきます。 実際に Node.js 18 をインストールしようとしたのですが、エラーになってしまいます。
具体的なエラーは /lib64/libc.so.6: version `GLIBC_2.14’ not found. Why am I getting this error? と一緒で、調べていくと Amazonlinux2 ベースである CentOS7 と Node.js 18 は nodeのv18を使ったらエラーになった(CentOS7) などにもあるように単純な話ではないようです。
おや?でも Lambda は Node.js 18 が使えるんですよね?どういうこと?と思いますよね。
Node.js のディストリビューション issue を確認すると distribution package Amazon Linux 2022 not supported なんてのがありました。
で、Amazon Linux 2022 って何なん?!
amazonlinux のコンテナイメージをみると、 latest
は amazonlinux2
なんですが、タグをみていくと確かに 2022
があります。
あと現時点では aws-cli のイメージは 2022 には追従していません。
そこで、 amazonlinux2022 をベースにして Node.js 18 をインストールするのを試してみます。
しかし、ここでプロビジョニングツールが amazonlinux 2022 に対応していないことがわかります(グルグル循環して脳が溶けてくる… 溶けてやがる、まだ早すぎたんだ….)。違うプロビジョニングツールも調べてみたのですが、どれも 2022 には対応していませんでした。ここでプロビジョニングツールを捨てる選択となりました。
(実際僕らは Chef を使っているんですが、Chef Workstation 自体に amazonlinux 2022 の対応は入っていました(Omnitruck artifact does not exist for version 17 on platform Amazon Linux 2022 )。ただまだリリースパッケージに含まれていないので、いずれリリースされるバージョンでは対応されているでしょう。Ansibleも調べましたが、同様にまだリリースパッケージには含まれていませんでした。)
調べていたら、AmazonLinux3じゃなくってAmazon Linux 2022 (AL2022) だってさ。 という記事が見つかりました。 そんで
AmazonLinux2022以降はメジャーバージョンが2年ごとにリリースされる
まじかー。まぁ今までの AmazonLinux2 が長かったですね。そのぐらいで OS イメージを最新にしていかないとですよね。 ということで、今後も踏まえて CI/CD のフローを含め利用するコンテナイメージを検討しなおすことにしました。 (2年ごとの変更がプロビジョニングツールを捨てる決定的な要因になったのは間違いない)
CI/CD フローの変更
現時点のフローを整理してみました
- チェックアウト
- ビルド
- デプロイが必要な場合
- 環境のスイッチロール
- デプロイ
だいたいこんな感じです。 これを今までは全部1つのコンテナでやっていたのですが、それぞれ分割していくことにしました。
- チェックアウト -> alpine/git
- ビルド -> amazon/aws-lambda-nodejs:18 ベースに zip/unzip を追加したもの
- デプロイが必要な場合
- 環境のスイッチロール -> amazon/aws-cli ベースに jq を追加したもの
- デプロイ -> node:18
ビルドに関しては node:18
でも良かったのですが、ここは以前と同じく稼働する OS イメージと合わせています。
今後 OS イメージを追従するというより、実際の Lambda 実行コンテナイメージを使うことで、 Node.js ランタイムにあった OS バージョンを気にせず利用できるメリットがあると考えました(サイズ大きいけど)。
それ以外は必要な部分に最低限のコンテナという感じですね。
さいごに
LTS に合わせて開発環境をアップデートしていくのは、とても大事ですね。 記事にすると、さらっと解決したように見えますが、それぞれのツールのソースコードや issue を確認しながら進んでいたので、かなり時間を取られてしまいました。 ただ、今後同じようにな環境でアップデートしていこうと思う人の助けになればと思います。
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