S3 のマルチパートアップロードは複数ファイルの集約にも使えちゃう

## S3 の Multipart upload とは
マルチパートアップロードは、単一のオブジェクトを複数に分割してアップロードできる機能です。分割された個々の部分 (part) は並列でアップロードできるため、スループットの向上やネットワークエラー時の影響軽減といった効果が期待できます。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/mpuoverview.html

大きなサイズのオブジェクトをアップロードまたはコピーする際にはマルチパートアップロードの使用が推奨されます。AWS CLI の高レベルコマンド (`aws s3`) ではファイルサイズに応じて自動的にマルチパートアップロードを実行するため、意識せずお世話になっている方も多いのはないかと思います。

https://repost.aws/ja/knowledge-center/s3-multipart-upload-cli

S3 のコスト最適化の文脈でも、不完全なマルチパートアップロードを削除するという設定でお目にかかるケースが多いですね。

## 複数ファイルの集約にも使える
マルチパートアップロードは単一のオブジェクトを複数に分割し、アップロードを効率化することを目的として提供されています。分割してアップロードされたファイルは最終的には元の単一オブジェクトに結合されるわけですが、それを利用して複数の別オブジェクトを単一のオブジェクトに集約することも可能です。

以降は AWS CLI を使用する場合の例です。
3 つのダミーファイルを作成し、マルチパートアップロードでファイルを結合します。この程度であればローカルで cat コマンドで連結してアップロードした方が速いし、楽やんというご指摘はごもっともですが、ここでは置いておいてください。

1. ダミーファイルの準備
パートファイルの最小サイズは 5 MB であるため、5 MB のファイルを 3 つ作成しておきます。
   ```shell
   dd if=/dev/zero of=file1.txt bs=5M count=1 && echo "hoge" >> file1.txt
   dd if=/dev/zero of=file2.txt bs=5M count=1 && echo "fuga" >> file2.txt
   dd if=/dev/zero of=file3.txt bs=5M count=1 && echo "piyo" >> file3.txt
   ```

1. マルチパートアップロードの開始
`create-multipart-upload` を実行し、レスポンスの `UploadId` を変数に格納します。
   ```shell
   BUCKET_NAME="your-bucket"
   TARGET_FILE="aggregated-file.txt"

   upload_id=$(aws s3api create-multipart-upload --bucket "${BUCKET_NAME}" --key "${TARGET_FILE}" | jq -r ".UploadId")
   ```

1. 各ファイルをアップロード
`upload-part` でパートファイルをアップロードします。レスポンスの ETag 値を JSON 形式で `parts` 変数に格納しています。これは後ほどマルチパートアップロードを完了する際に必要なリクエスト情報です。

   ```shell
   part_number=1   
   parts="{\"Parts\": ["

   while read part_file
   do
     etag=$(aws s3api upload-part --bucket "${BUCKET_NAME}" --key "${TARGET_FILE}" --body "${part_file}" --part-number ${part_number} --upload-id "${upload_id}" | jq -r ".ETag")
     parts="${parts}{\"ETag\": ${etag},\"PartNumber\": ${part_number}},"
     part_number=$((part_number+1))
   done <<EOF
   file1.txt
   file2.txt
   file3.txt
   EOF

   parts="${parts/%?/}"
   parts="${parts}]}"
   ```
   ちょっと無理やり感ありますね。。。

1. マルチパートアップロードの完了
`complete-multipart-upload` を実行してマルチパートアップロードを完了します。
   ```bash
   aws s3api complete-multipart-upload --multipart-upload "${parts}" --bucket "${BUCKET_NAME}" --key "${TARGET_FILE}" --upload-id "${upload_id}"
   {
      "ServerSideEncryption": "aws:kms",
      "SSEKMSKeyId": "arn:aws:kms:ap-northeast-1:123456789012:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "Location": "https://your-bucket.s3.ap-northeast-1.amazonaws.com/aggregated-file.txt",
      "Bucket": "your-bucket",
      "Key": "aggregated-file.txt",
      "ETag": "\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-x\""
   }
   ```

1. 結果確認
   15 MB の単一ファイルが作成されています。

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/2d2b2cff-292f-f6df-5602-6cdc7a2e88ec.png)
 
   ローカルにダウンロードし、中身を確認すると想定通り結合されていました。
   ```bash
   $ aws s3 cp s3://kosu/${TARGET_FILE} .
   download: s3://kosu/aggregated-file.txt to ./aggregated-file.txt 
   $ cat ./aggregated-file.txt 
   hoge
   fuga
   piyo
   ```

## まとめ
S3 のマルチパートアップロード機能で、複数のオブジェクトを単一のオブジェクトに集約できることを確認しました。が、本記事はこういった使い方を推奨するものでありません。

マルチパートアップロードは大きなサイズのオブジェクトアップロードを効率化するための機能です。単純にファイル集約を行う目的であれば、パフォーマンスやコスト面で最適ではありません。

例えばリクエストログをレコードごとにアップロードするなど、数 KB 程度の小さなサイズのファイルを大量にアップロードする場合は、S3 の Put 料金が高額になりがちです。

マルチパートアップロードアップロードで集約することを考えた場合、以下の点からこういったユースケースには明らかに向かないことがわかります。

* パートファイルの最小サイズが 5 MB
* パートファイルのアップロードは通常の put 料金がかかる

この場合、Amazon Kinesis Data Firehose でログデータをバッファリングして、S3 に配信する方がよいでしょう。

以上です。