TECHSCORE BLOG

クラウドCRMを提供するシナジーマーケティングのエンジニアブログです。

Amazon S3 の マルチパートアップロードを aws s3api で実行してみた

きっかけはある日の保守作業

Amazon S3 のバケットAから条件に合致する数十万件のオブジェクトを別AWSアカウントのバケットBにコピーする、という保守作業を数日間かけて実施しました。

  • コピー対象となったオブジェクトの件数
  • コピー先であるバケットBのオブジェクト件数

一致するはず、だったのですが・・・あれ?1件多いぞ、という問題が発生。

原因は単純で、コピー先であるバケットBに最初から1つオブジェクトが存在していたのを見逃していました。なぜ数が合わないの!?と大いに焦ってしまい、予想件数がそもそも合ってない、に思い至りませんでした。

オブジェクト数が予想と一致しないことは「あるある」なのか、検索すると AWS re:Post ですぐに記事がヒットしました。
CloudWatch メトリクスと Amazon S3 の AWS CLI ストレージメトリクスに不一致があるのはなぜですか?
記事では確認するべき点 2つについて触れられています。

  • オブジェクトのバージョニング
  • 不完全なマルチパートアップロード

オブジェクトのバージョニングに関しては何となく予想していましたが、マルチパートアップロードが不完全というのは経験がなく盲点でした。

不完全なマルチアップロードが残ってしまう問題について、AWSはバケットのライフサイクル設定を推奨しています。
不完全なマルチパートアップロードを削除するためのバケットライフサイクル設定の設定

Amazon S3 へのアップロードが失敗したオブジェクトがあるかもしれない、という場面に初めて遭遇し、そもそもマルチアップロードの詳細な処理の流れを知らないことで調査にあたふたしました。

マルチパートアップロードおよびダウンロードには、aws s3 cp などの aws s3 コマンドを使用するのがベストプラクティスです。これは、aws s3 コマンドがファイルサイズに応じてマルチパートアップロードとダウンロードを自動的に実行するためです。

とあるように、普段なら何も考えずに aws s3 cp を実行してマルチパートアップロードの恩恵を受けているわけですが、処理の流れを知っておく事は今後もスムーズに利用していく中で良いことだと思います。

aws s3 cp コマンドによるオートマチックな操作ではなく、aws s3api コマンドでオブジェクトを S3 にアップロードするための必要な操作を1つ1つ確認しながらマルチアップロードを実行してみました。

Amazon EC2 で AWS CLI を実行。権限等の理由で拒否されないユーザーで検証しています。

$ aws --version
aws-cli/2.9.2 Python/3.9.11 Linux/5.10.147-133.644.amzn2.x86_64 exe/x86_64.amzn.2 prompt/off

aws s3api でマルチアップロード

参考にした手順
AWS CLI を使用し、大きなファイルを複数に分割して Amazon S3 にアップロードするにはどうすれば良いですか?

準備

準備1から3までを実施しておきます。

準備1:ファイルを分割

高レベルな aws s3 cp で実行する場合は、ファイルサイズに応じて自動で分割されます。低レベルな aws s3api では、自分で分割しておく必要があります。
ここから始めるのか・・・と学習意欲が若干冷め気味になりましたが、準備は大切です。

  • アップロードファイル「test.txt」(以下のファイルパート2つを纏めたもの。1行目にファイルパート1,2行目にファイルパート2)
    • ファイルパート1「test.txt.00」:5MB を超えるサイズになるように全角文字を出力したファイル
    • ファイルパート2「test.txt.01」:「あいうえお」1行だけのファイル

「5MB を超える」とわざわざサイズを明記したのは、後で aws s3api complete-multipart-upload を実行する際にエラーになるためです。

An error occurred (EntityTooSmall) when calling the CompleteMultipartUpload operation: Your proposed upload is smaller than the minimum allowed size

分割するファイルパートの最小サイズは 5MB、ただし、最後のファイルパートはそれ以下でもOKです。

# ファイル一覧
$ ls -l | grep test.txt
-rw-rw-r-- 1 test-user test-user   5280104 Dec 19 13:25 test.txt
-rw-rw-r-- 1 test-user test-user   5280088 Dec 19 13:24 test.txt.00
-rw-rw-r-- 1 test-user test-user        16 Dec 19 13:04 test.txt.01

準備2:MD5 チェックサム値を計算

ファイルをアップロードする前に、アップロード後の整合性チェックの基準としてファイルの MD5 チェックサム値を計算しておきます。
実際にアップロードする 分割後のファイルパートの方を取得します。
OpenSSL コマンドを実行して、ファイルの Content-MD5 値を取得しました。 (base64 エンコーディングの適用をお忘れなく。)

# MD5 チェックサム値を計算
$ openssl md5 -binary test.txt.00 | base64
c1gbHJY6PNAA3wgskGq03Q==

$ openssl md5 -binary test.txt.01 | base64
I0ADMiaKzXnjzqqna8ZbUg==

準備3:アップロード ID を取得

マルチパートアップロードを開始して、関連付けられたアップロード ID を取得します。コマンドを実行すると、アップロード ID を含む応答が返されます。

# アップロード ID を取得
$ aws s3api create-multipart-upload --bucket <bucket-example> --key test.txt
{
    "ServerSideEncryption": "AES256",
    "Bucket": "<bucket-example>",
    "Key": "test.txt",
    "UploadId": "<examplevalue>"
}

<bucket-example> には実際のバケット名、key には分割前のファイル名を指定しました。
<examplevalue> は払い出されたアップロード ID です。

準備はここまでです。

手順

実際にマルチパートアップロードを実行します。

手順1:ファイルパートをアップロード

ファイルパートをアップロードして、アップロードされたファイルの一部である ETag 値を含む応答を取得します。

# ファイルの最初のパートをアップロード
$ aws s3api upload-part --bucket <bucket-example> --key test.txt --part-number 1 --body test.txt.00 --upload-id <examplevalue> --content-md5 c1gbHJY6PNAA3wgskGq03Q==
{
    "ServerSideEncryption": "AES256",
    "ETag": "\"73581b1c963a3cd000df082c906ab4dd\""
}

<bucket-example> にはバケット名、key には分割前のファイル名、body には分割後のファイルパート名を指定しました。
upload-id<examplevalue>content-md5 の MD5 チェックサム値は準備で取得した値です。

分割したファイルパート数だけ繰り返して実行します。今回は2個に分割したので、次で最後のファイルパートです。

# ファイルの最後のパートをアップロード
$ aws s3api upload-part --bucket <bucket-example> --key test.txt --part-number 2 --body test.txt.01 --upload-id <examplevalue> --content-md5 I0ADMiaKzXnjzqqna8ZbUg==
{
    "ServerSideEncryption": "AES256",
    "ETag": "\"23400332268acd79e3ceaaa76bc65b52\""
}

すべてのファイルパートをアップロードしたら、次のコマンドを実行し、アップロードされたパートの一覧を表示して、一覧が完全なものであることを確認します。

# アップロードされたパートの一覧を表示して、一覧が完全なものであることを確認
$ aws s3api list-parts --bucket <bucket-example> --key test.txt --upload-id <examplevalue>
{
    "Parts": [
        {
            "PartNumber": 1,
            "LastModified": "2023-12-19T05:02:24+00:00",
            "ETag": "\"73581b1c963a3cd000df082c906ab4dd\"",
            "Size": 5280088
        },
        {
            "PartNumber": 2,
            "LastModified": "2023-12-19T05:02:44+00:00",
            "ETag": "\"23400332268acd79e3ceaaa76bc65b52\"",
            "Size": 16
        }
    ],
    "ChecksumAlgorithm": null,
    "Initiator": {
        "ID": "arn:aws:sts::999999999999:assumed-role/test-role/i-1234567890abcdefg",
        "DisplayName": "test-role/i-1234567890abcdefg"
    },
    "Owner": {
        "DisplayName": "test-test-test",
        "ID": "1234567890123456789012345678901234567890123456789012345678sample"
    },
    "StorageClass": "STANDARD"
}

<bucket-example> にはバケット名、key には分割前のファイル名、upload-id<examplevalue>は準備で取得した値です。
問題なさそうです。

次のコマンドでは、未完了のマルチファイルパートのアップロード一覧を取得しています。

# 未完了のマルチファイルパートのアップロードを一覧取得
$ aws s3api list-multipart-uploads --bucket <bucket-example>
{
    "Uploads": [
        {
            "UploadId": "<examplevalue>",
            "Key": "test.txt",
            "Initiated": "2023-12-19T05:00:19+00:00",
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "test-test-test",
                "ID": "1234567890123456789012345678901234567890123456789012345678sample"
            },
            "Initiator": {
                "ID": "arn:aws:sts::999999999999:assumed-role/test-role/i-1234567890abcdefg",
                "DisplayName": "test-role/i-1234567890abcdefg"
            }
        }
    ]
}
aws s3 で検証
# 一覧取得しても何も無し
$ aws s3 ls s3://<bucket-example>

手順2: マルチパートアップロードを実行

アップロードした各ファイルパートの ETag 値を JSON 形式のファイルに設定しておきます。

$ cat fileparts.json | jq .
{
  "Parts": [
    {
      "ETag": "73581b1c963a3cd000df082c906ab4dd",
      "PartNumber": 1
    },
    {
      "ETag": "23400332268acd79e3ceaaa76bc65b52",
      "PartNumber": 2
    }
  ]
}

ファイル名は fileparts.json としました。
マルチパートアップロードを実行します。--multipart-upload の値はこのファイル名のパスです。

# マルチパートアップロードを実行
$ aws s3api complete-multipart-upload --multipart-upload file://fileparts.json --bucket <bucket-example> --key test.txt --upload-id <examplevalue>
{
    "ServerSideEncryption": "AES256",
    "Location": "https://<bucket-example>.s3.ap-northeast-1.amazonaws.com/test.txt",
    "Bucket": "<bucket-example>",
    "Key": "test.txt",
    "ETag": "\"9f84f5941a157cad4bcf11e742a20940-2\""
}

<bucket-example> にはバケット名、key には分割前のファイル名、upload-id<examplevalue>は準備で取得した値です。

aws s3api で検証

不完全なマルチパートアップロードは起きていないことを確認できました。

# アップロードされたパートの一覧は存在しない
$ aws s3api list-parts --bucket <bucket-example> --key test.txt --upload-id <examplevalue>

An error occurred (NoSuchUpload) when calling the ListParts operation: The specified upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.

# 未完了のマルチファイルパートのアップロードを一覧取得しても何も無し
$ aws s3api list-multipart-uploads --bucket <bucket-example>
aws s3 で検証

ファイルがアップロードされていることを確認できました。

# 一覧取得
$ aws s3 ls s3://<bucket-example>
2023-12-19 14:01:45    5280104 test.txt

# --summarize オプション付きで一覧取得
$ aws s3 ls s3://<bucket-example> --recursive --summarize
2023-12-19 14:01:45    5280104 test.txt

Total Objects: 1
   Total Size: 5280104

最後にファイルをダウンロードして、ファイルの内容を検証します。

# ファイル名「test.txt.from_s3」でダウンロード
$ aws s3 cp s3://<bucket-example>/test.txt test.txt.from_s3
download: s3://<bucket-example>/test.txt to ./test.txt.from_s3

# ファイル一覧
$ ls -l | grep test.txt
-rw-rw-r-- 1 test-user test-user   5280104 Dec 19 13:25 test.txt
-rw-rw-r-- 1 test-user test-user   5280088 Dec 19 13:24 test.txt.00
-rw-rw-r-- 1 test-user test-user        16 Dec 19 13:04 test.txt.01
-rw-rw-r-- 1 test-user test-user   5280104 Dec 19 14:01 test.txt.from_s3

# 同じファイルであることを確認
$ diff -s test.txt test.txt.from_s3
Files test.txt and test.txt.from_s3 are identical

同じファイルであることを確認できました。

最後に

きっかけとなったある日の保守作業の「オブジェクトの数が1件多い」の原因ですが、コピー先であるバケットBに最初から1つオブジェクトが存在していた、というものでした。
aws s3 ls でオブジェクト一覧を取得したところ、明らかにタイムスタンプが古いオブジェクトに気が付いた時には膝から崩れ落ちそうになりましたが、マルチパートアップロードについて理解を深めるきっかけになったのは良かったと思います。

「確認は大事」(作業前にバケットがカラであることを確認するべきだった)、
「都合がよさそうな結論に飛びつかない」(「不完全なマルチアップロードが残るとストレージメトリクスに不一致がある」と AWS ドキュメントに記載されている)
を肝に銘じて、慣れた保守作業であっても慎重に対応していこうと思います。

梶原 知恵(カジハラ チエ)
息をするように typo するおかげで、閉じ括弧「 } 」忘れに時間を取られる不幸なプログラマー。
好物はビールとワインとお砂糖。苦手はハンバーグと焼くと蜜が出てくる系統のサツマイモと、匂いに特徴がある発酵食品(納豆はダメ)。


シナジーマーケティング株式会社では一緒に働く仲間を募集しています。