maybe daily dev notes

私の開発日誌

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

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

LambdaのNode.js 18ランタイムではAWS SDK v3のみプリインストールされているなど、いよいよAWS SDK for JSを移行すべき状況になっている。 しかし私は未だにSDK v3でS3のファイルをダウンロード/アップロードする操作に慣れないので、この記事にまとめる。

事前準備

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

npm install @aws-sdk/client-s3

コード

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

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

それぞれのコードを下記のとおりである。

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

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. ファイルシステム上からアップロード

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',
  })
);

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

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

対象のファイルサイズが大きな場合は、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,  // 分割時のサイズ 全体の分割数(合計サイズ/partSize)が10000を超えないようにする
});

await upload.done();

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

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

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 bytes = await s3Object.Body?.transformToByteArray();
const str = await s3Object.Body?.transformToString();

少し前までこれが少し面倒だったのが最近楽になった件は、ここにも書いた。

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

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

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にコピー/移動

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の指定方法がすこし特殊になる。詳細はこちら

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

なお、CopyObject APIを使えるのはファイルサイズが5GBまで。5GBを超えるファイルは、コピー元ファイルをダウンロードし、コピー先にアップロードする必要がある。

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: {
    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のファイルを扱うときのコードをまとめた。