S3バケットから最終更新日が古いオブジェクトを洗い出してリスト化してみた
はじめに
こんにちは、カスタマーサクセス部 クラウド運用チームの momi です。
数十万オブジェクトが格納されたS3バケットの棚卸しを行う機会がありました。
AWS CLIとシェルスクリプトを使い、「最終更新日時が古いオブジェクト」をバケット直下のフォルダ単位で集計し、削除候補のリストを作成したので、そのスクリプトと出力内容を紹介します。
なお、本記事で扱うのは「何がどれだけ存在するのか」を可視化するところまでとなっており、削除作業そのものは対象外です。
背景
S3バケットの容量超過を知らせるCloudWatchアラームBucketSizeBytesが発報しており、バケット内にどんなデータがどれくらい溜まっているか実態を把握する必要がありました。
同じくCloudWatchのストレージメトリクスNumberOfObjectsを確認すると、オブジェクト数は数十万件ほど存在していました。
S3コンソールの「オブジェクト」タブはフォルダ階層を1階層ずつ表示する仕様のため、この規模になるとコンソールから実態を掴むのは厳しいと感じました。
また、実際に削除する際は事前確認が必要だったため、対象を見つけるだけでは足りず、以下の2つを揃える必要がありました。
- サマリ:どのフォルダにどれぐらいのサイズのものが何件あるか(依頼文に記載する内訳)
- 詳細:対象ファイルの一覧(証跡として添付するもの)
そこで、削除候補となる古いオブジェクトを洗い出しつつ、この2つを一度に出力できる形でスクリプトを組んでみました。
なお、全オブジェクトの一覧を取得する手段としてはS3 Inventoryもありますが、有効化にあたって出力先バケットの用意やバケットポリシーの設定が必要で、調査のために環境へ設定を追加することになります。
初回レポートの配信まで時間がかかる点もあり、今回は単発の棚卸しだったこともあって、その場で実行できるCLIを選びました。
※BucketSizeBytesはストレージクラスごとに発行されるメトリクスですが、今回のバケットはすべてS3標準クラスのため、StandardStorageの値をそのまま全体容量として扱っています。
対象を洗い出す
条件を決める
削除対象の基準は、「最終更新日時が3ヶ月より前のオブジェクト」(フォルダ・ファイル種別による区別なし)としました。
日付の指定は相対指定にしたいので、シェルのdateコマンドで実行時点から3ヶ月前の日付を計算します。
集計スクリプト
#!/bin/bash
BUCKET="example-bucket-log"
THRESHOLD=$(date -v-3m +%Y-%m-%d 2>/dev/null || date -d '3 months ago' +%Y-%m-%d)
echo "しきい値(3ヶ月前): $THRESHOLD"
# 前回実行分の詳細ファイルが残っていると二重に追記されるため削除
rm -f old_objects_detail_*.tsv
aws s3api list-objects-v2 --bucket "$BUCKET" \
--query 'Contents[].[Key,Size,LastModified]' --output text \
| awk -F'\t' -v threshold="$THRESHOLD" '{
split($1,a,"/");
top=a[1];
datepart=substr($3,1,10);
if (datepart < threshold) {
count[top]++;
size[top]+=$2;
# フォルダごとに別ファイルへ書き出す
file="old_objects_detail_" top ".tsv";
print $1"\t"$3 >> file
}
}
END {
print "TopFolder\tObjectCount\tTotalSizeBytes" > "old_objects_summary.tsv"
for (k in count) print k"\t"count[k]"\t"size[k] >> "old_objects_summary.tsv"
}'
# サマリー:見出し固定+ソート
{ head -n1 old_objects_summary.tsv; tail -n +2 old_objects_summary.tsv | sort; } > tmp_summary && mv tmp_summary old_objects_summary.tsv
# 詳細:サマリーに出てきた各フォルダをループして、見出し追加+ソート
echo "---- 詳細ファイル一覧 ----"
tail -n +2 old_objects_summary.tsv | while IFS=$'\t' read -r folder cnt sz; do
f="old_objects_detail_${folder}.tsv"
if [ -f "$f" ]; then
{ echo -e "Key\tLastModified"; sort "$f"; } > "${f}.tmp" && mv "${f}.tmp" "$f"
echo "$(pwd)/$f"
fi
done
echo "サマリー保存先: $(pwd)/old_objects_summary.tsv"
※バケット名、および後述する出力例のフォルダ名・オブジェクトキー・数値はすべてサンプルです。
スクリプトの処理内容
list-objects-v2で取得したKey / Size / LastModifiedをawkに渡し、キーの先頭(トップフォルダ)ごとに件数とサイズを集計しています。
LastModifiedとしきい値(3ヶ月前の日付)を比較し、それより古いものだけを対象として、フォルダ別の詳細ファイルと全体のサマリーファイルの両方に書き出します。
なおlist-objects-v2が返すのは現行バージョンのみです。今回のバケットはバージョニングが有効なため、非現行バージョンは本スクリプトの集計対象外となります。
また、list-objects-v2が返すLastModifiedはUTC、dateコマンドが返すのはローカルタイム(JST)の日付です。加えてdateの「3ヶ月前」の計算結果は実行環境(GNU / BSD)によっても変わります。
これらの理由で境界日付近では数日のズレが生じることがありますが、今回は「3ヶ月より前」という粗い基準での棚卸しなので、この程度の誤差は許容しています。
実行結果
実行すると、フォルダ(トップ階層)ごとに「対象件数」「対象サイズ」をまとめたtsvファイルが出力されます。
TopFolder ObjectCount TotalSizeBytes
device-A 6000 5800000000
device-B 20000 22000000000
device-C 1500 1230000000
device-D 100 300000000
device-E 650 450000000
device-F 1000 90000000
※フォルダ名および数値はサンプルです。
実際のケースでも同様に、特定のフォルダに件数・容量が集中している状態でした。
あわせて、フォルダ別の詳細ファイル(ファイル名+最終更新日時の一覧)も同時に出力されるので、対象の詳細を個別に確認することが可能です。
Key LastModified
device-A/Log/20260115/eventlog.log 2026-01-15T06:21:27+00:00
device-A/Log/20260115/driver0.log 2026-01-15T10:04:18+00:00
device-A/Log/20260115/driver1.log 2026-01-15T13:24:08+00:00
サマリーは依頼文に記載する内訳として、詳細ファイルはxlsxにまとめて対象一覧の証跡として、それぞれそのまま使うことができました。
最後に、この集計の限界についても触れておきます。
NumberOfObjectsは非現行バージョン・削除マーカー・未完了マルチパートアップロードのパートも含む一方、list-objects-v2が返すのは現行バージョンのみです。
特に、未完了マルチパートアップロードのパートはlist-objects-v2に現れないのにBucketSizeBytesには計上されるため、容量超過の原因として見落としがちです。
まとめ
- 数十万オブジェクト規模になると、S3コンソールでは実態を把握できない。CLIで
list-objects-v2+awkのような形で集計するのが現実的 list-objects-v2の結果をawkに1回処理するだけで、バケット直下のフォルダ単位の集計とフォルダ別の詳細リストを同時に作成できる- CloudWatchのメトリクスとCLIの集計値は母数が異なる。
NumberOfObjectsは非現行バージョン・削除マーカー・未完了マルチパートアップロードのパートを含み、BucketSizeBytesはストレージクラス別に発行される点に注意が必要
実際に削除する場合は、バージョニングやライフサイクルルールとの兼ね合いも含めて別途検討が必要になりますが、まずは「何がどれだけ存在するのか」を可視化するところまでを今回はまとめました。
同じように大量のオブジェクトの中から最終更新日で抽出したい方の参考になれば幸いです。





