[Node.js] [AWS SDK v3] StreamでCSVファイルをS3からS3にコピーする

2022.03.31

広島の吉川です。

Node.js Streamに入門してみた | DevelopersIO

さっき書いたこちらの記事の発展として、S3を使ったStream処理をやってみたいと思います。

【S3からS3へ】Node.js の Streaming API を使って Lambda Function のみで CSVファイルを JSON Lines ファイルへ変換する | DevelopersIO

ちょっとこちらの真似をして、「S3から別のS3にCSVをコピーする」というスクリプトを書いてみたいと思います。大きく違うのはAWS SDKのバージョンで、参考元記事はv2で、本記事はv3でやっていきます。

環境

  • node 16.14.0
  • typescript 4.6.3
  • @aws-sdk/{client-s3,lib-storage} 3.58.0
  • csv 6.0.5

S3のWriteStream

まずS3にCSVをアップロードする処理を行っていきます。

v2だとS3クライアントから .upload() というメソッドが生えていたのですが、v3には同じものがなさそうでした。

node.js - How to upload a stream to S3 with AWS SDK v3 - Stack Overflow

こちらのStackOverflowによると、 @aws-sdk/client-s3 の他に @aws-sdk/lib-storage パッケージを組み合わせることで同等処理の実現ができるようです。そのように書いていきます。

import { S3Client } from '@aws-sdk/client-s3'
import { Upload } from '@aws-sdk/lib-storage'
import { generate } from 'csv'

const region = 'ap-northeast-1'
const s3Client = new S3Client({
  region,
})
const bucketName = 'YOUR_BUCKET_NAME'
const keyName = 'example.csv'

const main = async () => {
  const csvStream = generate({ length: 5000000 })

  const upload = new Upload({
    client: s3Client,
    params: {
      Bucket: bucketName,
      Key: keyName,
      Body: csvStream,
    },
  })
  upload.on('httpUploadProgress', (progress) => {
    console.log(progress)
  })
  await upload.done()
}

main()

Body 引数に stream.Readable を渡せるようになっているので、そのまま使わせてもらう形です。

S3のReadStream

AWS SDK v3ではなんと最初から GetObjectOutput.Bodystream.Readable になっています。なので、普通に扱えばReadStreamでの読み取りになります。

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

const region = 'ap-northeast-1'
const s3Client = new S3Client({
  region,
})
const bucketName = 'YOUR_BUCKET_NAME'
const keyName = 'example.csv'

const main = async () => {
  const output = await s3Client.send(
    new GetObjectCommand({
      Bucket: bucketName,
      Key: keyName,
    })
  )

  ;(output.Body as Readable).pipe(fs.createWriteStream('example.csv'))
}

main()

ただ、 Bodystream.Readable で返す仕様が逆に利用者を混乱させている面もあるようで、下記のようなIssueが建っていました。

S3.GetObject no longer returns the result as a string · Issue #1877 · aws/aws-sdk-js-v3

たしかにそれほど大きくないテキストファイルなら最初から string 型などで取得できた方が楽な場合も多そうです。上のIssueによると string に変換したい場合はget-streamというライブラリを使うのが簡単そうです。

Streamを使ってS3から別のS3へCSVをコピーする

では、あるS3バケットのCSVファイルを別のS3バケットにコピーするという処理を書いていきます。

今回は362MBのCSVファイルをS3バケット1に入れておきました。

これをS3バケット2にコピーするというシナリオでコードを書きます。

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

const region = 'ap-northeast-1'
const s3Client = new S3Client({
  region,
})
const bucketName1 = 'YOUR_BUCKET_NAME_1'
const keyName1 = 'example.csv'
const bucketName2 = 'YOUR_BUCKET_NAME_2'
const keyName2 = 'copied-example.csv'

const main = async () => {
  const getObjectOutput = await s3Client.send(
    new GetObjectCommand({
      Bucket: bucketName1,
      Key: keyName1,
    })
  )

  const upload = new Upload({
    client: s3Client,
    params: {
      Bucket: bucketName2,
      Key: keyName2,
      Body: getObjectOutput.Body,
    },
  })
  upload.on('httpUploadProgress', (progress) => {
    console.log(progress)
  })
  await upload.done()
}

main()

前回と同じ方法で計測したところメモリ使用量は約244.7MBでした。

Streamを使わずS3から別のS3へCSVをコピーする

比較のためにあえてStreamでデータを流すことを避けたコードも書きました。それが下記です。

import {
  GetObjectCommand,
  PutObjectCommand,
  S3Client,
} from '@aws-sdk/client-s3'
import getStream from 'get-stream'
import { Readable } from 'stream'

const region = 'ap-northeast-1'
const s3Client = new S3Client({
  region,
})
const bucketName1 = 'YOUR_BUCKET_NAME_1'
const keyName1 = 'example.csv'
const bucketName2 = 'YOUR_BUCKET_NAME_2'
const keyName2 = 'copied-example.csv'

const main = async () => {
  const getObjectOutput = await s3Client.send(
    new GetObjectCommand({
      Bucket: bucketName1,
      Key: keyName1,
    })
  )
  const csvString = await getStream(getObjectOutput.Body as Readable)

  await s3Client.send(
    new PutObjectCommand({
      Bucket: bucketName2,
      Key: keyName2,
      Body: csvString,
    })
  )
}

main()

さきほど紹介したget-streamを使って stream.Readablestring に変換した後、アップロードするようにしてみました。

こちらも計測したところメモリ使用量は約1568.5MBでした。

まとめ

ケース メモリ使用量
Streamを使ってS3から別のS3へCSVをコピーする 244.7MB
Streamを使わずS3から別のS3へCSVをコピーする 1568.5MB

こちらの例でもStreamを使うことでメモリ使用量を抑えることができました。

参考