オブジェクト数が大量のバージョニングされたS3バケットを削除する方法

## はじめに

オブジェクト数が2,000万のS3バケットを消すのは中々大変でした
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94938/2b55ef62-1028-74ce-211d-3c36008c728f.png)

バージョニングされていない場合は、AWS CLIで

```shell
aws s3 rb s3://${bucket} --force
```
でオブジェクトを削除しつつ消すことができます。時間はかかりますが。

バージョニングされている場合、全てのオブジェクトを消した後も

```
An error occurred (BucketNotEmpty) when calling the DeleteBucket operation: The bucket you tried to delete is not empty. You must delete all versions in the bucket.
```
と表示されます。オブジェクトを削除しても、過去バージョンに残っているので、バケットが空ではないということです。

## 削除するのに時間がかかっても良い場合

以下のようなS3のライフサイクルルールをつけて放置して空になるのを待つことをおすすめします。

```json:lifecycle.json
{
    "Rules": [
        {
            "ID": "delete-old-objects",
            "Status": "Enabled",
            "Prefix": "",
            "Expiration": {
                "Days": 1
            },
            "NoncurrentVersionExpiration": {
                "NoncurrentDays": 1
            }
        }
    ]
}
```

```shell
aws s3api put-bucket-lifecycle-configuration --bucket ${bucket} \
  --lifecycle-configuration file://lifecycle.json
```

バージョニングされたバケットの場合、オブジェクトの削除でも `DeleteMarkers` というバージョン扱いになってしまうため、1日経過してオブジェクトの有効期限が切れる→1日経過して削除されたというバージョンも失効する、という2段構えでいずれバケットが空になります。

## 少しでも早くS3バケットを削除したい！！

今回、削除対象のS3バケットが複数あったので、上記のライフサイクルルールを複数のS3バケットに一斉に付与して待っていました。

```shell
aws s3 ls | grep "削除したいS3バケット群がバケット名に含む文字列" | cut -d' ' -f3 | \
xargs -I{} aws s3api put-bucket-lifecycle-configuration --bucket {} --lifecycle-configuration file://lifecycle.json
```

2〜3日で全てのS3バケットが削除できるようになるのかな、と思っていたのですが、4日経ってもまだオブジェクトが残っていて削除できないバケットがありました。
S3のライフサイクルルールの有効期限切れは、即座に適用されるのではなく `削除キューに追加され、非同期的に削除されます。` とのことです。
あまりにも多くのS3バケットとオブジェクトのライフサイクルルールを設定したので、全部が消せるようになるまではかなり時間がかかりそうでした。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/lifecycle-expire-general-considerations.html

> 期限切れのオブジェクトに関連付けられている有効期限切れ、またはストレージ期間に対する料金は請求されません。

とのことだったので、料金的には削除による節約ができている状態なのですが、消そうと思ったS3バケットがいつ消せるのかということを気にしつづけるのは脳のメモリを食うので、できるだけ早く消したいと思い、バージョニングされたオブジェクトを消す方法を調べました。

### バージョニングされたS3バケットのオブジェクト数が1,000以内の場合

`aws s3api list-object-versions` でバージョニングされたオブジェクトのキー・バージョンを取得して `aws s3api delete-objects` を実行するのですが、以下のワンライナーで削除できます。

```shell
## バージョニングされたオブジェクトの一覧を取得して削除
aws s3api delete-objects --bucket {} --delete \
"$(aws s3api list-object-versions --bucket {} --query='{Objects: Versions[].{Key:Key,VersionId:VersionId}}')"
## 削除としてマークされたオブジェクトの一覧を取得して削除
aws s3api delete-objects --bucket {} --delete \
"$(aws s3api list-object-versions --bucket {} --query='{Objects: DeleteMarkers[].{Key:Key,VersionId:VersionId}}')"
```
こちらはクラスメソッドさんの記事でも紹介があります。

https://dev.classmethod.jp/articles/aws-cli-remove-files-versioning-s3-bucket/

ただ、この `list-object-versions` の結果が大きすぎると、 `Argument list too long` というエラーが出て失敗します。
その場合は、Queryで1,000件に絞ってあげると上手くいきます。

```shell
# バージョニングされたオブジェクトの一覧を1,000件取得して削除
aws s3api delete-objects --bucket {} --delete "$(aws s3api list-object-versions --bucket {} \
--query='{Objects: Versions[:1000].{Key:Key,VersionId:VersionId}}')"
# 削除としてマークされたオブジェクトの一覧を1,000件取得して削除
aws s3api delete-objects --bucket {} --delete "$(aws s3api list-object-versions --bucket {} \
--query='{Objects: DeleteMarkers[:1000].{Key:Key,VersionId:VersionId}}')"
```

オブジェクト数が数千〜数万程度であれば、このコマンドを何度か叩いたりfor文でループすれば良さそうです。

```shell
for i in {0..20} \
do \
  aws s3 ls | grep "削除したいS3バケット群がバケット名に含む文字列" | cut -d' ' -f3 \
  | xargs -I{} sh -c 'aws s3api delete-objects --bucket {} --delete \
  "$(aws s3api list-object-versions --bucket {} \
  --query='\''{Objects: Versions[:1000].{Key:Key,VersionId:VersionId}}'\'')"';\
done
```

### バージョニングされたS3バケットのオブジェクト数が数千万のときはどうすれば…？

まず、数千万のオブジェクト数があるバケットは、 `list-object-versions` がめちゃくちゃ遅いので、上記for文 -> xargsの中での1コマンドが5分くらい返ってきません。1,000件削除するために5分だと、2,000万件削除しようとすると果てしない感じがしました。

ちなみに削除対象が何件存在するかを確認するのはこんな感じです。このコマンドも `list-object-versions` で全件取得してカウントしているので時間がかかります。

```shell
# バージョニングされたオブジェクトの一覧の数を表示
aws s3api list-object-versions --bucket ${bucket} --query "Versions" | jq "length"
# 削除としてマークされたオブジェクトの一覧の数を表示
aws s3api list-object-versions --bucket ${bucket} --query "DeleteMarkers" | jq "length"
```


毎回 `list-object-versions` で取得する必要はなく、削除対象のパラメータを1,000件ごとに区切って作成することができれば良いので、一度全件取得してJSONに出力した後、1,000件ずつパラメータに指定して削除しました。
ちなみにオブジェクト数が2,000万超えのバケットの `list-object-versions` のレスポンスをJSONに保存すると、サイズは6.2GBでした…😇
以下はDeleteMarkersが大量に残っている場合ですが、 `DeleteMarkers` を `Versions` に変更すれば同様に可能です。

```shell
# 対象のバケットのlist-object-versionsをファイルに書き出す
aws s3 ls | grep "削除したいS3バケット群がバケット名に含む文字列" | cut -d" " -f3 \
| xargs -P 4 -I{} sh -c 'aws s3api list-object-versions --bucket {} \
--query "DeleteMarkers" > {}-delete-markers.json'

# APIのレスポンスがページングされて処理が止まらないようにする
export AWS_PAGER=""
# 上記で出力したJSONを1ファイルずつ読み込んで1,000件ずつ削除
aws s3 ls | grep "削除したいS3バケット群がバケット名に含む文字列" | cut -d" " -f3 \
| while read bucket ;\
do \
  loop_count=$(python -c "print(int($(cat ${bucket}-delete-markers.json | jq "length") / 1000 + 1))"); \
  for i in {0..$loop_count};\
  do \
    start=$(expr $i \* 1000); \
    end=$(expr $start + 1000); \
    echo "$bucket: $i / $loop_count"; \
    aws s3api delete-objects --bucket ${bucket} --delete \
    "$(jq "{Objects: [.[$start:$end][] | {Key,VersionId}]}" ${bucket}-delete-markers.json)"; \
  done
done
```

これでバージョニングされたオブジェクトと削除としてマークされたオブジェクトをS3バケットから一掃したら、最初に紹介した以下のコマンドでS3バケットを削除できます🎉
```shell
aws s3 ls | grep "削除したいS3バケット群がバケット名に含む文字列" | cut -d" " -f3 | xargs -I{} aws s3 rb s3://{} --force
```