AWSCLI、Python(boto3)などからS3フォルダを削除してみる

2018.06.29

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

マネジメントコンソールやAWSCLIからは比較的簡単にS3フォルダを指定して削除できますが、Pythonプログラム(boto3)で同じことを試みると、削除対象のオブジェクトのリストが取得した後、個々のオブジェクトを削除するプロブラムを書く必要がありました。毎回関数を作成するのは面倒なので、いざというときに備えて作成しましたのでご紹介します。

検証用データ(ファイル)の作成

検証用フォルダ・ファイルをのポチポチ作成するのは無理なので以下のスクリプトで、2999個のファイルを一括作成します。s3-delete-folderバケットの下のmy_folder/folder_1というフォルダ階層の下に、test_1 〜 test_2999 のファイルを作成します。

import boto3

bucket_name = "s3-delete-folder"
folder_name = "my_folder/folder_1/test_"

s3 = boto3.resource('s3')

for i in range(1, 3000):
    obj = s3.Object(bucket_name,folder_name + str(i))
    body = "s3 object test."
    r = obj.put(Body = bytearray(body))

マネジメントコンソールからS3フォルダの削除

ますはマネジメントコンソールからS3フォルダの削除する方法をおさらいします。フォルダ名を右クリックして、[削除]を選択します。

すると、ダイアログが表示されますので、確認した後、[削除]ボタンを押します。

以降では、これと同じことを AWSCLI と Python(boto3)から行います。

AWSCLIでS3フォルダの削除する

S3フォルダ名を指定するとあたかも削除できたように見えますが、確認すると削除されません。

$ aws s3 rm s3://s3-delete-folder/my_folder/folder_1
delete: s3://s3-delete-folder/my_folder/folder_1

$ aws s3 ls s3://s3-delete-folder/my_folder/folder_1
PRE folder_1/

S3フォルダ名を指定して削除するには、--recursiveを指定します。S3フォルダごと削除されたことが確認できます。

$ aws s3 rm s3://s3-delete-folder/my_folder/folder_1 --recursive
delete: s3://s3-delete-folder/my_folder/folder_1/test_100
delete: s3://s3-delete-folder/my_folder/folder_1/test_1
delete: s3://s3-delete-folder/my_folder/folder_1/test_10
delete: s3://s3-delete-folder/my_folder/folder_1/test_1000
:
:
delete: s3://s3-delete-folder/my_folder/folder_1/test_999

$ aws s3 ls s3://s3-delete-folder/my_folder/folder_1
$

Python(boto3)でS3フォルダを削除する方法

S3フォルダをまとめて削除するには

S3フォルダをまとめて削除するには、まずファイルの一覧を取得した後、オブジェクトごとに削除を実行する必要があります。しかし、バケットとキー指定してオブジェクトの一覧を取得する関数(list_objects や list_objects_v2)では、一度に最大1000個までしか取得できません。そのため1000以上のオブジェクトを考慮して、リストの終わりまで繰り返し処理するような実装が必要です。

オブジェクトの一覧を取得する関数は2種類あり、従来のlist_objects関数 と 現在のlist_objects_v2関数の2種類ありますので、それぞれのAPIで一覧を取得し、オブジェクトを削除する関数を作成しました。新規でスクリプトを作成する場合は、現在のlist_objects_v2関数で構いませんが、比較的古いBoto3の場合は、従来のlist_objects関数を用いた関数を利用したほうが良いでしょう。

[推奨]list_objects_v2関数によるS3フォルダの削除

現在のAPI(list_objects_v2)では取得されたオブジェクト件数が、リクエストパラメータで指定されたMaxKeysか、デフォルト値(1000)以上存在した場合には、 NextContinuationTokenという値がレスポンスで返却されていました。その値を次のAPIコールにContinuationTokenパラメータとして与えることで残りの値を取得することができます。v1のサポートも続けられていますが、今後の新規開発ではv2を利用することが推奨されています。

バケット名(s3-delete-folder)とプレフィックス(my_folder/folder_1/)を指定して実行します。プレフィックスはフォルダを指定するので、プレフィックスの最後の文字はスラッシュ「/」にしてください。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import boto3

bucket = "s3-delete-folder"
prefix = "my_folder/folder_1/"

def delete_all_keys_v2(bucket, prefix, dryrun=False):
    contents_count = 0
    next_token = ''

    while True:
        if next_token == '':
            response = s3client.list_objects_v2(Bucket=bucket, Prefix=prefix)
        else:
            response = s3client.list_objects_v2(Bucket=bucket, Prefix=prefix, ContinuationToken=next_token)

        if 'Contents' in response:
            contents = response['Contents']
            contents_count = contents_count + len(contents)
            for content in contents:
                if not dryrun:
                    print("Deleting: s3://" + bucket + "/" + content['Key'])
                    s3client.delete_object(Bucket=bucket, Key=content['Key'])
                else:
                    print("DryRun: s3://" + bucket + "/" + content['Key'])

        if 'NextContinuationToken' in response:
            next_token = response['NextContinuationToken']
        else:
            break

    print(contents_count)

if __name__ == "__main__":
    s3client = boto3.client('s3')
    delete_all_keys_v2(bucket, prefix, True)

list_objects_v2関数の詳細については、以下のブログを御覧ください。

S3 ListObjects APIの新バージョン ListObjectsV2を使おう

[非推奨]旧API list_objects関数によるS3フォルダの削除

従来のAPI(list_objects)では取得されたオブジェクト件数が、リクエストパラメータで指定されたmax-itemsか、デフォルト値(1000)以上存在した場合には、NextMarkerという値がレスポンスで返却されていました。その値を次のAPIコールにmarkerパラメータとして与えることで残りの値を取得することができました。

バケット名(s3-delete-folder)とプレフィックス(my_folder/folder_1/)を指定して実行します。プレフィックスはフォルダを指定するので、プレフィックスの最後の文字はスラッシュ「/」にしてください。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import boto3

bucket = "s3-delete-folder"
prefix = "my_folder/folder_1/"

def delete_all_keys(bucket='', prefix='', dryrun=False):
    contents_count = 0
    marker = None

    while True:
        if marker:
            response = s3client.list_objects(Bucket=bucket, Prefix=prefix, Marker=marker)
        else:
            response = s3client.list_objects(Bucket=bucket, Prefix=prefix)

        if 'Contents' in response:
            contents = response['Contents']
            contents_count = contents_count + len(contents)
            for content in contents:
                if not dryrun:
                    print("Deleting: s3://" + bucket + "/" + content['Key'])
                    s3client.delete_object(Bucket=bucket, Key=content['Key'])
                else:
                    print("DryRun: s3://" + bucket + "/" + content['Key'])

        if response['IsTruncated']:
            marker = response['Contents'][-1]['Key']
        else:
            break

    print(contents_count)

if __name__ == "__main__":
    s3client = boto3.client('s3')
    delete_all_keys(bucket, prefix, True)

番外編: ライフサイクルルールでファイル削除

オブジェクトが大量にある場合、それらを逐一削除するのは辛いので、ライフサイクルマネジメントで自動的に削除(Cleanup)する方法もおすすめです。

最後に

S3フォルダを削除するだけなら、マネジメントコンソールやAWSCLIで事足りますが、AWS Glueなどで複数の依存関係のあるジョブやジョブ間でフロー制御しようとすると、Python(boto3)プログラムとして自動化する必要があります。そのようなユースケースを想定してPython(boto3)でS3フォルダを削除する方法をご紹介しました。なので、削除対象ファイルが何億あっても動作するようにしたつもりです。なお、実際のプログラムに組み込む際には、DryRunを実行してコピー対象の一覧を確認して、コピー対象の確認を怠らないでください。

なお、何度も「S3フォルダ」なんて言ってましたが、フォルダなんてないものはないことは承知しています。

Amazon S3における「フォルダ」という幻想をぶち壊し、その実体を明らかにする

合わせて読みたい

AWSCLI、Python(boto3)などからS3フォルダ間のコピーしてみる