maybe daily dev notes

私の開発日誌

AWS SDK JavaScript v3でS3のファイル操作 チートシート

ワタミチートシート以来、久々のカンペ記事。

AWS SDK JavaScript v3がリリースされて久しいが、移行は進んでいるだろうか? LambdaのNode.js v18ランタイムではv3 SDKのみビルトインされているなど、そろそろ移行を進めないとまずい状況も増えてきている。

私自身は未だにv3のSDKでS3のファイルをダウンロード/アップロードする操作に慣れないので、この記事にそれらのサンプルコードをまとめる。

事前準備

以降のコードに必要なライブラリは、基本的に以下の一つだけでOK。

npm install @aws-sdk/client-s3

コード

S3頻出のパターンとして、以下5つがあるだろう:

  1. メモリ上のデータをS3にアップロード
  2. ファイルシステムのデータをS3にアップロード
  3. メモリ上にS3のデータをダウンロード
  4. ファイルシステムにS3のデータをダウンロード
  5. S3上のファイルを他のS3バケットコピー/移動

それぞれのサンプルコードを以下に示す。

1. メモリ上からアップロード

以下は、some data という文字列を test.txt としてアップロードする例。文字列以外にもBufferなどが使える。

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

const s3 = new S3Client({});

// Bodyのデータをtest.txtとしてアップロード
await s3.send(
  new PutObjectCommand({
    Body: 'some data',  // Bufferなども指定可
    Bucket: process.env.BUCKET_NAME,
    Key: 'test.txt',
  })
);

2. ファイルシステム上からアップロード

以下は、test.txt というファイルをアップロードする例。

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { createReadStream } from 'fs';

const s3 = new S3Client({});

// ローカル上のtest.txtをアップロード
await s3.send(
  new PutObjectCommand({
    Body: createReadStream('test.txt'),
    Bucket: process.env.BUCKET_NAME,
    Key: 'test.txt',
  })
);

ファイルを一度メモリ上に展開する方法も考えられるが、ReadStreamを使うほうが省メモリで済む。

備考: 対象のファイルサイズが大きな場合

対象のファイルサイズが大きな場合は、Multipart機能を使うことでより高速にアップロードできる場合がある。これを便利に使うための機能がSDK v3にはあるので、追加でインストールする。

npm i @aws-sdk/lib-storage

コードは以下:

import { S3Client } from '@aws-sdk/client-s3';
import { Upload } from "@aws-sdk/lib-storage";
import { createReadStream } from 'fs';

const s3 = new S3Client({});
const upload = new Upload({
  client: s3,
  params: {
    Body: createReadStream('sample.bin'),
    Bucket: process.env.BUCKET_NAME,
    Key: 'sample.bin',
  },
  // 性能改善用の細かなパラメータ
  queueSize: 10,  // アップロードの並列数
  partSize: 1024 * 1024 * 5,  // 1パート当たりのサイズ
});

await upload.done();

手元で500MB程度のファイルをアップロードしたところ、およそ40%速くなった。色々な条件にも依ると思うため、参考までに。こちらも参照

3. メモリ上にダウンロード

/test.txt というオブジェクトをメモリ上に展開する例:

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';

const s3 = new S3Client({});

// S3上のtest.txtをダウンロード
const s3Object = await s3.send(
  new GetObjectCommand({
    Bucket: process.env.BUCKET_NAME,
    Key: 'test.txt',
  })
);
const str = await s3Object.Body?.transformToString();
// バイト列として取得したいときはこちら
// const bytes = await s3Object.Body?.transformToByteArray();

ちなみに、最近この操作が上記のように楽になった (以前はStreamを意識したコードが必要だった) 件は、ここにも書いた。

TIL: AWS SDK for JavaScript v3 で s3.GetObject する最新の方法 - maybe daily dev notes

4. ファイルシステム上にダウンロード

/test.txt というオブジェクトをファイルシステム上に test.txt というファイルとして保存する例:

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import {createWriteStream } from 'fs';
import { Readable } from 'stream';

const s3 = new S3Client({});

// S3上のtest.txtをダウンロードし./test.txtに保存
const s3Object = await s3.send(
  new GetObjectCommand({
    Bucket: process.env.BUCKET_NAME,
    Key: 'test.txt',
  })
);
await new Promise((resolve, reject) => {
  if (s3Object.Body instanceof Readable) {
    s3Object.Body.pipe(createWriteStream('test.txt'))
      .on('error', (err) => reject(err))
      .on('close', () => resolve(0));
  }
});

メモリからディスクに都度書き出す、省メモリな実装。この場合は依然としてStreamを繰る必要がある。

5. S3からS3にコピー/移動

あるバケットtest.txt を 別バケットtest_copy.txt としてコピー・移動する例:

import { S3Client, CopyObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';

const s3 = new S3Client({});

// test.txtをtest_copy.txtにコピー 
await s3.send(
  new CopyObjectCommand({
    Bucket: process.env.TARGET_BUCKET_NAME,
    Key: 'test_copy.txt',
    CopySource: `${process.env.SOURCE_BUCKET_NAME}/test.txt`
  })
);

// 移動の際は元ファイルを削除
await s3.send(
  new DeleteObjectCommand({
    Bucket: process.env.SOURCE_BUCKET_NAME,
    Key: 'test.txt',
  })
)

コピー元であるCopySourceの指定方法がすこし特殊 ( s3:// などではない)。詳細はこちら

ちなみにオブジェクトを移動したい場合は、コピー後に元オブジェクトを削除する。2つの操作をアトミックに実行するS3 APIは今のところ存在しないので、整合性が重要な場合は要注意 (あまりないと思うが。)

なお、CopyObject APIを使えるのはファイルサイズが5GBまで。5GBを超えるファイルは、コピー元オブジェクトをダウンロードしながらコピー先にアップロードする必要がある。Streamのおかげでコードは簡単かつメモリ上で完結する。

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';

const s3 = new S3Client({});

// 5GBを超えるファイルをコピーする
const s3Object = await s3.send(
  new GetObjectCommand({
    Bucket: process.env.SOURCE_BUCKET_NAME,
    Key: 'sample.bin',
  })
);

// 大きなファイルで高効率なMultipartアップロードを利用
const upload = new Upload({
  client: s3,
  params: {
    // streamを直接渡せる
    Body: s3Object.Body,
    Bucket: process.env.TARGET_BUCKET_NAME,
    Key: 'sample_copy.bin',
  },
});

await upload.done();

注意

いくつかの疑問が生じたので、ついでに調べた:

  • @aws-sdk/client-s3S3S3Client の2つあるが、どっち使えば良い?

このドキュメントに詳しく書かれている。S3 はv2と似た体験を実現するために用意されたもの。これを使うと、s3.putObject のように xxCommand クラスを使わずにAPIを呼べる。

一方でTree shaking観点ではこの古い方法はイマイチらしく、フロントエンドなどバンドルサイズの要求がシビアな場面では S3Client を使う方法が好まれる。個人的には書き方を使い分けるのも面倒なので S3Client の方で統一するのが良いと思うが、バックエンドでの利用などバンドルサイズが多少大きくても問題ない場合は書きやすい昔の記法もアリだろう。

なおこれはS3に限らず、DynamoDBやEC2など他のすべてのサービス用SDKで共通の話。

  • new S3({}){}、必要?

必要。型定義上この引数を省略することはできない。理由は不明だが、オプショナルにするとnullチェックが追加で必要になるので、多少気持ちはわかる。このIssueで提案はされたが、特に対応されなかった模様。

以上、AWS SDK for JS v3でS3のファイルを扱うときのコードをまとめた。