delete markerと非現行バージョン
一言でいうと
バージョニングを有効にすると、削除は消すことではなく、「消したという印(delete marker)」をもう1つ載せることになります。元のデータはそのまま残り、料金も出続けます。
なぜ必要なのか
S3は長いあいだ、結果整合性でした。オブジェクトを上書きしてすぐ読むと、古い値が返ることがあり、このために多くのアプリケーションがリトライロジックを入れました。2020年12月から、AWS S3は強い読み取り-書き込みの整合性を提供しています。PUT直後のGETが、新しい値を見ます。
ただし、この保証の範囲を正確に知っておく必要があります。
| 操作 | 現在の保証 | 注意 |
|---|---|---|
| PUTのあとのGET(新しいキー) | 強い整合性 | — |
| PUTのあとのGET(上書き) | 強い整合性 | — |
| DELETEのあとのGET | 強い整合性 | バージョニングを有効にすると、404のあとにデータが残っています |
| LIST | 強い整合性 | ほかの実装は、たいてい、ここで弱いです |
| マルチリージョンレプリケーション | 非同期 | レプリカは、いつでも遅れることがあります |
「S3は今は強い整合性」という文を、すべてのS3互換ストレージにそのまま当てはめてはいけません。MinIOは強い整合性ですが、Ceph RGWは設定によって違い、SeaweedFSは、filerの構成によって、一覧の取得が遅れることがあります。このラボのストレージも同じです。自分が書くコードがどんな保証の上に立っているかは、ドキュメントではなく実測で確認します。
条件付きリクエストで競合を防ぐ
強い整合性があっても、2つのプロセスが同じキーに同時に書く問題は残ります。あとに書いたものが勝つので、前の更新が黙って消えます。S3は、これを条件付きヘッダーで防ぎます。
# 없을 때만 만든다 — 있으면 412 Precondition Failed
s3.put_object(Bucket=b, Key=k, Body=data, IfNoneMatch="*")
# 내가 읽은 그 버전일 때만 덮어쓴다(낙관적 잠금)
head = s3.head_object(Bucket=b, Key=k)
s3.put_object(Bucket=b, Key=k, Body=new, IfMatch=head["ETag"])
IfMatchが412を返したら、その間に誰かが変更したということです。もう一度読み直してマージし、リトライします。このパターンがないと、「同時に保存を押したら、1人分だけが残る」事故が起きます。
バージョニングは別の問題を解く
バージョニングが解くのは、整合性ではなく、誤って上書きしたり削除したりしたものを元に戻す問題です。
バージョニングを有効にすると、同じキーに書くたびに、新しいバージョンIDができます。最新のものが現行バージョンで、残りは非現行バージョンです。古いバージョンは、バージョンIDでいつでも読めます。
削除が興味深いところです。バージョンIDなしでDELETEすると、実際には削除されず、delete markerという特殊なバージョンが、一番上に載ります。そのため、通常のGETは404を受け取りますが、データはその下にそのまま残っています。
키 report.csv 의 버전 스택 (위가 최신)
delete marker (v4) ← 지금 GET 하면 404
────────────────────
2026-09-05 판 (v3) ← v4 를 지우면 이것이 다시 현행이 된다
2026-09-04 판 (v2)
2026-09-03 판 (v1)
delete markerを削除すると、すぐ下のバージョンが、再び現行になります。これが、ミスからの復旧の仕組みです。本当に削除するには、バージョンIDを明示してDELETEする必要があり、この操作は元に戻せません。
もう1つ。バージョニングは、有効にはできても、無効にはできません。Suspendedの状態にしかできず、すでに溜まったバージョンはそのまま残ります。Suspendedの状態で書いたオブジェクトは、バージョンIDがnullの特殊なバージョンになり、次にまた書くと、それを上書きします。有効にする前に、ライフサイクルルールを先に決める理由です。
現場での姿
バージョニングのコストは、よく見落とされます。ログファイルを毎日同じキーに上書きするコードにバージョニングを有効にすると、1年後に、そのキー1つに365個のバージョンが溜まっています。一覧ではオブジェクト1つに見えるのに、料金は365倍です。そして、「全部消したのに料金がそのまま」という状況も、delete markerのせいです。
隠れたコストがもう1つあります。失敗したマルチパートアップロードです。5GBのファイルをアップロードしていて、80%で切れると、すでにアップロードされたパートが、どこにも見えないまま課金されます。一覧にも出ず、バージョン一覧にも出ません。list-multipart-uploadsでだけ見えます。
そのため、バージョニングを有効にするときは、必ずライフサイクルルールを一緒にかけます。3つのルールが、基本セットです。
{"Rules": [
{"ID": "expire-noncurrent", "Status": "Enabled", "Filter": {},
"NoncurrentVersionExpiration": {"NoncurrentDays": 30}},
{"ID": "clean-delete-markers", "Status": "Enabled", "Filter": {},
"Expiration": {"ExpiredObjectDeleteMarker": true}},
{"ID": "abort-incomplete-mpu", "Status": "Enabled", "Filter": {},
"AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 7}}
]}
ルールは1日1回、非同期で動くので、すぐには反映されません。適用の翌日から容量が減り始めれば、正常です。
元に戻せない削除を、そもそも防ぐ必要があるデータには、Object Lockを使います。GOVERNANCEモードは、特別な権限があれば解除でき、COMPLIANCEモードは、ルートアカウントでも期間内は削除できません。監査対象のログには、後者を使います。
次のラボですること
バージョニングを有効にして、同じキーに3回書いてバージョンを溜め、古いバージョンを読み、削除後のdelete markerを確認し、それを除去して復旧し、非現行バージョンが占める容量を計算したあと、ライフサイクルルールを適用します。最後に、条件付きPUTが412を返すことを、自分で確認します。