AWS CLIのs3 cpやs3 syncでアップロードするバケットには不完全なマルチアップロードの削除設定を検討しておこう

2024.06.25

初めに

S3へのアップロード方法の一種であるマルチパートアップロードはファイルを複数に分割しアップロードすることでスループットの向上、エラーによって中断された際の途中からの再開等のメリットがあります。

一方で完了しなかったマルチパートアップロードの不完全なパートに関しては考える必要があり、不完全なパートはマネジメントコンソールではデフォルトでは確認することができないものの確保された容量から請求に繋がります(Storage Lensを利用することで可能ではあるが別途有効化・料金)。なお不完全なパートについてはAbortMultipartUploadAPIを呼ぶことで削除可能です。

仕様上アプリ開発者側がSDK経由でよく利用するであろうPutObjectを呼んでも自動的にマルチパートアップロードが実行されることはなく意図的にCreateMultipartUploadを呼ぶ必要はありますが、各種ツールでは実は利用されているような場合もあります。

身近な例としてAWS CLIのs3 cpおよびs3 syncサブコマンドとなります。

aws s3コマンド

aws s3コマンドはAWS APIに対して直接マッピングされたコマンドではなくそれらをラップした高レベルのサブコマンドとなります。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-services-s3-commands.html
aws s3 コマンドを使用して大きなオブジェクトを Amazon S3 バケットにアップロードする場合、 AWS CLI は自動的にマルチパートアップロードを実行します。これらの aws s3 コマンドを使用した場合、失敗したアップロードを再開することはできません。

これらを経由したファイルアップロードを行うとファイルサイズによってはPutObject操作ではなくマルチパートアップロード処理に変換され実行されますことがあります。
(デフォルトでは8MiBがライン)

良くも悪くも速度が出せるようにコマンドがよしなにラップされているため知識が少なくともある程度最適化されたアップロード処理は見込めるものの不完全なパートが意図せず生成されてしまうリスクもあります。

絶対不完全なパートアップロードのまま残ってしまうか?と言われるとそういうわけではなく手動キャンセルや何らかの実行失敗時にはAbortMultipartUploadが実行されるためゴミデータができる限り残らないようには作成されてはいます。

## bigfileは事前にddコマンドで作成した3GBほどのファイル
## Ctrl + Cでキャンセル
% aws s3 cp bigfile s3://xxxxxx-bucket/
^Ccancelled: ctrl-c received
% aws s3api list-multipart-uploads --bucket xxxxxx-bucket
{
    "RequestCharged": null
}

そのため多くの場合は不完全なパートの残留は回避できますが、これは必ずではありません。

絶対ではないAbort処理

このAbort処理はあくまでクライアント側からの指示により行われるという性質上呼び出され成功することは保証されているわけではありません。

例えばCLI実行ユーザにs3:AbortMultipartUploadの権限が割り当てられていない場合呼び出しがあっても権限がないため削除できません。何気にこの場合特にエラーは出ません。

## AbortMultipartUploadをDenyするポリシーをつけて実行
 % aws s3 cp bigfile s3://xxxxxxxx-bucket/
^Ccancelled: ctrl-c received
% aws s3api list-multipart-uploads --bucket xxxxxxx-bucket
{
    "Uploads": [
        {
            "UploadId": "RxoS5SDp43D_.C4EOjIFHW6_YWs9dFn1ov0arOwnrVW1UPNb0vnoBFCJtaocbn0W7POsklGB0KgEj269yT0o8eLBG.aSpONmWLaU89eYGb3Peo4hNTabUHpLo7SSyxVVG.hBRYp2TCPU_sUvW2E8PQ--",
            "Key": "bigfile",
            "Initiated": "2024-06-24T08:40:38+00:00",
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "xxxxxx",
                "ID": "0e494268e138245416252d9dfe288f53ceff50c3d0a88f30efac0328d9477aa5"
            },
            "Initiator": {
                "ID": "arn:aws:iam::xxxxxx:user/xxxxxx",
                "DisplayName": "xxxxxx"
            }
        }
    ],
    "RequestCharged": null
}

またAbortMultipartUpload権限が割り当てらていても呼び出し処理自体が行われない場合同様に残ってしまいます。

# killで例外処理を挟むまもなく落とす
 % aws s3 cp bigfile s3://xxxxxx-bucket/
#----ここから別タブ
% ps | grep "s3 cp"
 3403 ttys006    0:04.29 aws s3 cp bigfile s3://xxxx-bucket/
% kill 3403
#----ここまで別タブ
zsh: terminated  aws s3 cp bigfile s3://xxxxx-bucket/g
## 例外処理に入る間もなく落とされるので削除処理が行えず残り続ける
% aws s3api list-multipart-uploads --bucket xxxxx-bucket
{
    "Uploads": [
        {
            "UploadId": "qxyZKr9Ya44pEjheKrtBvAlorpz7swI5G.6OOaLsUgz5K8ss0Jm32rAhWkMjkyRPPbaiT9R_AYDnU4FAkUYR03hGQ3CvhOGOh3MBbbwA7unUPo4p8IazUVZYVueVdb27iDGggm.sS.Qo8HhkWlGuYQ--",
            "Key": "bigfile",
            "Initiated": "2024-06-24T08:48:21+00:00",
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "xxxxxx",
                "ID": "0e494268e138245416252d9dfe288f53ceff50c3d0a88f30efac0328d9477aa5"
            },
            "Initiator": {
                "ID": "arn:aws:iam::xxxxx:user/xxxxxx",
                "DisplayName": "xxxxx"
            }
        }
    ],
    "RequestCharged": null
}

## 転送処理中にネットを切ってネットワーク的にAbort指示が到達できないようにする(長期のネットワーク障害想定)
 % aws s3 cp bigfile s3://xxxxxx-bucket/
upload failed: ./bigfile to s3://xxxxx-bucket/bigfile Could not connect to the endpoint URL: "https://xxxxx-bucket.s3.ap-northeast-1.amazonaws.com/bigfile?uploadId=Pru.A1rC8qIUGCYm.zBw_3SSWWJwb25x58mqED5X8tyn.S1zoalluIsv_vd2Fyb_A..0kP3MYN846bO0yV17UMZyOYIbGMJWVFISR7kS1tAr.kRwIFvHz1rmj0QRgoTmTQUSStG1yShTMWkrwAYHow--&partNumber=20"
% aws s3api list-multipart-uploads --bucket xxxxx-test-bucket
{
    "Uploads": [
        {
            "UploadId": "Pru.A1rC8qIUGCYm.zBw_3SSWWJwb25x58mqED5X8tyn.S1zoalluIsv_vd2Fyb_A..0kP3MYN846bO0yV17UMZyOYIbGMJWVFISR7kS1tAr.kRwIFvHz1rmj0QRgoTmTQUSStG1yShTMWkrwAYHow--",
            "Key": "bigfile",
            "Initiated": "2024-06-24T08:53:32+00:00",
            "StorageClass": "STANDARD",
            "Owner": {
                "DisplayName": "xxxxx",
                "ID": "0e494268e138245416252d9dfe288f53ceff50c3d0a88f30efac0328d9477aa5"
            },
            "Initiator": {
                "ID": "arn:aws:iam::xxxxx:user/xxxxx",
                "DisplayName": "xxxxx"
            }
        }
    ],
    "RequestCharged": null
}

マルチパートアプロードの仕様としては失敗した以降の差分をアップロードすることで継続可能ですが、s3 cpにはそのような指定はないため難しく基本的には最初からやり直す形となるめ、この不要となったパートはユーザ側で削除が必要となります。

別途AbortMultipartUploadAPIを呼ぶことでも可能ですがライフサイクルで作成後不完全な状態のまま維持されている日数が一定を超えると削除される「不完全なマルチパートアップロードを削除」を指定することでも対応可能です。
ライフサイクルの設定自体には料金はかからず発生したオペレーションのみに料金がかかるのでとりあえず設定しておくというのも一つの手となります。

ただしごく短い日数を選択してしまうと他処理で作成されたパートで復旧処理用に保持されているようなものが削除されてしまう可能性もありますので、あくまで消し忘れ防止のための程度余裕を持った日数にしておくのが良いでしょう。

終わりに

改めてs3 cpで作業を行う際の注意点に少し触れてみました。良い感じにAPIがラップされて簡単に使えるコマンドではあるのでちょっとした作業の利用にも良く使われ、またAWS初心者の方も触れることの多いコマンドでかと思いますが見えない部分で不要なものができるところもあるので頭の片隅に置いておくと良いです。

マルチパートアップロードはAWS CLIに限らず現在では書き込み処理ができるようになったMountpoint for Amazon S3などS3を使う各種ミドルウェア・ソフトウェアなどでもよしなに利用されており呼び出されているケースもあります。

適切に開発されていればAbort自体はするようになっていることは多いかと思いますが必ずではないですし、今回紹介したいくつかの例のように落ち方によってはAbortされないケースもあるためもしかして?と思う環境があれば一度チェックをして気になるようであればライフサイクルで不完全なマルチパートを削除しておくようにするのが良いでしょう。

周辺の利用状況によっては別途再開用に適切に管理されている不完全なパートを削除してしまうこともあるため期間の設定は十分ご注意ください。