🗝️

AWS SDK for JavaScript v3 におけるクレデンシャル設定の最適解: キャッシュメカニズムとベストプラクティス

2024/06/02に公開

背景

JavaScript用 AWS SDK において v2 から v3 でクライアント設定のアプローチが変更されました。v2ではSDKそのものに共通設定を行えましたが、v3 ではクライアントのインスタンス毎にランタイム設定を持つようになりました。そのため、v2 では各AWSリソースのクライアントを無作為に生成しても内部的に設定がキャッシュされていましたが、v3 ではクレデンシャルの取得が多発し、問題が表面化することがあります。

クレデンシャル取得のロジックを深堀る

最もシンプルな方法は、AWSリソースのクライアントのコンストラクタ引数に credentials としてAWSアクセスキーとシークレットアクセスキーを直接セットすることです。しかし、実際のアプリケーション開発では、この方法はあまり一般的ではありません。EC2やECSにデプロイされたアプリケーションは、インスタンスやコンテナのメタデータサービスから取得します。Lambdaなどは環境変数経由で取得します。こういったクレデンシャル供給ロジックを Credential Provider として AWS SDK v3 は実装しています。

クライアントの credentials には Credential Provider を指定することも可能です。詳細は以下のページに記載されています。
https://github.com/aws/aws-sdk-js-v3/tree/main/packages/credential-providers

credentials に何も指定しない場合、まず credentialDefaultProvider にセットされた関数を実行して Credential Provider を生成して credentials とします。デフォルトの credentialDefaultProviderdefaultProvider になります。これは各種取得方法をに試みます。この方式を使うことでローカル環境では ~/.aws/credentials を参照し、デプロイ環境ではメタデータを参照するということを自動的に行ってくれます。

defaultProvider の実装を明示的に指定するには fromNodeProviderChain を使用します。

クレデンシャルのキャッシュメカニズム

クレデンシャルの取得はクライアントが各コマンドを呼び出すときに行いますが、AWS SDK が提供する各取得ロジックを実装した Credential Provider にはキャッシュ機構はありません。唯一、defaultProvider の内部で @smithy/property-provider の memoize がキャッシュをしています。 さらに、セッショントークンを含む期限付きクレデンシャルの再取得ロジックもここだけに実装されています

https://github.com/aws/aws-sdk-js-v3/blob/main/packages/credential-provider-node/src/defaultProvider.ts#L59

https://github.com/smithy-lang/smithy-typescript/blob/main/packages/property-provider/src/memoize.ts#L46

ベストプラクティスは何か

ここまで読み解くと、同一AWSアカウント内で完結するアプリケーションを実装する場合、ほとんどのケースで「defaultProviderを使う」、つまり「何も指定しない」がベストということになります。しかし、ここに落とし穴があります。

冒頭に述べたように、v3 ではクライアント毎に Credential Provider が異なるため、キャッシュもクライアント単位で行われます。例えばバッチ処理のループの中で「クライアントを生成してアイテムを取得する」というコードだと、例えば、バッチ処理のループ内で「クライアントを生成してアイテムを取得する」コードの場合、クレデンシャル取得が失敗する可能性があります。以下は、EC2 のメタデータエンドポイントのアクセスリミットに達してエラーになったというIssueです。

https://github.com/aws/aws-sdk-js-v3/issues/4867

これを回避する方法として、各AWSクライアントのインスタンス自体をキャッシュ(使いまわす)することがIssueのコメントにも書かれています。ただし、多くのAWSサービスに依存しているアプリケーションで、それらへのコマンド送信が集中的に発生する場合には、回避できない可能性があります。これに対する改善提案もあります。

https://github.com/aws/aws-sdk-js-v3/issues/4612

結論

現状では defaultProvider を使いまわすのがベストということです。fromNodeProviderChain が SDK から提供されているので、下記のようにモジュールレベルで生成しておいて使いまわしましょう。

import { fromNodeProviderChain } from "@aws-sdk/credential-providers";

export const credentialProvider = fromNodeProviderChain();
new S3Client({ credentials: credentialProvider });

new DynamoDBClient({ credentials: credentialProvider });

これで内部で取得処理が集中して行われることを防げます。


参考記事

https://zenn.dev/luma/articles/bd3c59b3d7682d

Discussion