はじめに
AWSのハンズオンでS3バケットにバージョニングを有効化してみたら、「有効化」自体は一瞬なのに、そのあとに知っておくべきことが想像より多いと気づきました。削除したのにファイルが消えない、請求が静かに増える、一度ONにすると戻せない——このあたりは公式ドキュメントを読むまでピンときませんでした。
この記事は、S3バージョニングの「有効化したあとの世界」を初学者向けに整理した学習メモです。仕組み(3つの状態・削除マーカー・null バージョン)から、実務で効いてくるコスト管理(ライフサイクル)まで、図を交えて順に見ていきます。
忙しい人向けの結論(3行):
- バージョニングは「上書き・誤削除しても過去版が残る」保険。ただし一度有効化すると未設定には戻せない(停止まではできる)
- 削除しても「削除マーカー」が乗るだけで中身は残る。だから復元しやすい反面、放置すると過去版のストレージ課金が積み上がる
- 入れっぱなしにせず、ライフサイクルルールで「古い版を自動で整理」するところまでがワンセット
参考リンク
S3バージョニングの概要(AWS公式)
削除マーカーとバージョン削除の挙動(AWS公式)
ライフサイクルで過去版を減らしてコストを下げる(AWS Storage Blog)
ライフサイクルと他のバケット設定の相互作用(MFA Delete との併用不可・AWS公式)
aws_s3_bucket_versioning リソース(Terraform Registry)
そもそもバージョニングって何?
ひとことで言うと、同じ名前(キー)のオブジェクトを上書き・削除しても、前の版を消さずに取っておく機能です。Wordの変更履歴やGitのコミット履歴に近いイメージ。
バージョニングなしのS3だと、photo.jpg を新しい内容で上書きした瞬間、古い photo.jpg は消えます。誤って古いファイルを上げてしまっても、もう戻せない。バージョニングを有効にしておくと、上書きのたびに新しい版が積まれ、古い版は version_id 付きでそのまま残ります。
何がうれしいのか。誤削除・誤上書きからの復旧はもちろん、最近はランサムウェア対策の文脈でも語られます。暗号化されてしまった版の「一つ前」に戻せるからです。
バージョニングには3つの状態がある
ここが最初の「へえ」ポイントでした。バージョニングは ON/OFF の2択ではなく、3つの状態を持ちます。
- 未設定(Unversioned): バケット作成時のデフォルト。version_id は付かない
- 有効(Enabled): 上書き・削除しても過去版が残る。各版に version_id が付く
- 停止(Suspended): 新しく入れるオブジェクトは version_id が null になる。ただし、すでにある過去版は消えずに残る
一番の注意点はこれです。一度「有効」にしたバケットは、「未設定」には二度と戻せません。やめたいときにできるのは「停止(Suspend)」までで、停止は新しい版を作らなくするだけ。過去に溜まった版は残り続けます(=課金も続く)。だから「とりあえずON」の前に、後述のコスト管理まで頭に入れておくと安全です。
「削除したのに消えない」謎 ―― 削除マーカーの正体
自分が最初につまずいたのがこれでした。バージョニング有効なバケットでオブジェクトを削除したのに、バージョン一覧に残っていて「消えてないじゃん」と混乱したんです。あなたも同じところで引っかかっていませんか?
種を明かすと、version_id を指定しない普通の削除(DELETE)は、オブジェクトを物理削除しません。代わりに削除マーカーという“フタ”を最新版として乗せるだけなんです。
- GET すると 404 が返る(=アプリからは「消えた」ように見える)
- でも過去版(v1, v2 …)は生きている
- だから削除マーカーを外せば、そのまま元に戻せる
「じゃあ本当に消したいときは?」——そのときは version_id を指定して削除します。指定した版だけがストレージから物理削除され、これは元に戻せません。
つまりバージョニング中の削除には2種類あるわけです。フタをするだけの「普通の削除」と、版を名指しで消す「完全削除」。この違いが分かると、次のコストの話がスッと入ってきます。
version_id が "null" になるやつ
version_id まわりでもう一つ混乱しやすいのが null バージョンの存在です。次の2ケースで version_id が null になります。
- 有効化する前にバケットへ置いていたオブジェクト(後から有効化しても、その版の id は null のまま)
- 停止(Suspended)状態のときに新しく入れたオブジェクト
null 版はキーごとに1つしか持てず、同じキーで入れ直すと上書きされます。Terraform でハンズオンしていると、「バージョニング有効化」と「オブジェクトのアップロード」の順番しだいで version_id が null になってしまう、という順序依存に出くわします。この対処は最後のTerraform節で触れます。
AWSは「バケットのバージョニングを初めて有効化したら、オブジェクトへの書き込み(PUT/DELETE)を始める前に15分ほど待つこと」を推奨しています。有効化直後に一気に書き込むと、設定が行き渡る前の版が意図せず null 扱いになることがあるためです。
落とし穴:バージョンは黙って課金され続ける
ここが実務でいちばん効いてくる話です。過去版(noncurrent version)も、当然ながらS3のストレージ課金の対象になります。
バージョニングが便利なのは「消しても残る」から。でもそれは裏を返すと、上書きや削除を繰り返すほど、見えないところで版が積み上がり、ストレージ料金がじわじわ増えるということでもあります。同じキーに何度もデプロイするバケットや、ログを上書き保存するバケットだと、気づいたときには過去版が本体の何倍にもなっていた——というのはよく聞く事故です。
前の削除マーカーの話ともつながります。普通に消したつもりでも版は残るので、意識して整理しない限りストレージは減りません。
対策:ライフサイクルで“世代管理”する
放置すると増えるなら、自動で整理すればいい。それがS3のライフサイクルルールです。バージョニングとライフサイクルは、ほぼセットで組むものだと思っておくと良いです。
よく使う組み合わせはこの2つ。
- 古い版の失効(NoncurrentVersionExpiration): 過去版になってから N 日たったら自動で完全削除する
- 最新◯世代だけ残す(newer_noncurrent_versions): 直近の版は守りつつ、それより古い版だけ消す
たとえば「過去版は30日で失効、ただし最新3世代は残す」とすれば、直近のロールバック余地を確保しつつ、古い版の課金は止められます。消すのが惜しい版は、削除ではなくGlacierなどの安いストレージクラスへ移す(transition)手もあります。
失効した版のストレージ料金はかかりません。ただし実際の削除は非同期で、失効のマークが付いてから物理削除まで少しラグがあります。「今すぐ全部消えて課金ゼロ」ではなく「マークが付いた時点で課金対象から外れる」と理解しておくと安心です。
もう一段の守り:MFA Delete
もっと堅くしたいバケットには MFA Delete という仕組みがあります。版の完全削除や、バージョニング状態の変更(停止など)に、MFA(多要素認証)のコードを要求する設定です。
- 効果: 誤操作や、認証情報を奪われたときの「版ごと消される」攻撃を防ぎやすい
- クセ: 有効化できるのはバケット所有者(ルートユーザー)だけで、マネジメントコンソールからは設定できず、CLI/API から行う必要がある
普段の学習では必須ではありませんが、「消されたら本当に困る」バケット(監査ログ、バックアップなど)では検討する価値があります。
一つ大事な制約があります。MFA Delete とライフサイクル設定は、同じバケットで併用できません。ライフサイクルが有効なバケットに MFA Delete を付けようとすると InvalidBucketState(Mfa Authentication is not supported on a bucket with lifecycle configuration)で弾かれます。つまり「古い版はライフサイクルで自動削除しつつ、削除には MFA も要求する」は同一バケットでは成立しません。自動整理を取るか、MFA による削除ガードを取るか——用途ごとにバケットを分けて設計します。
Terraform での有効化(ハンズオン)
最後に、Terraform での最小コードを置いておきます。自分はこの構成でハンズオンしました。バージョニングは aws_s3_bucket 本体ではなく、独立した aws_s3_bucket_versioning リソースで設定します。
# S3バケット名は全世界で一意が必要。random_id で衝突を避ける
resource "random_id" "suffix" {
byte_length = 4
}
resource "aws_s3_bucket" "site" {
bucket = "handson-versioning-${random_id.suffix.hex}"
}
# バージョニングを有効化
resource "aws_s3_bucket_versioning" "site" {
bucket = aws_s3_bucket.site.id
versioning_configuration {
status = "Enabled"
}
}
そして、さきほど触れた「順序依存」の対処です。バージョニングの有効化リソースより先にオブジェクトがアップされると version_id が null になってしまうので、depends_on でアップロードを後回しにします。
resource "aws_s3_object" "index" {
bucket = aws_s3_bucket.site.id
key = "index.html"
source = "index.html"
# バージョニング有効化のあとに上げる(先に上げると version_id=null になる)
depends_on = [aws_s3_bucket_versioning.site]
}
depends_on が保証するのは Terraform 上の作成順序(並列適用で先に上がってしまうのを防ぐ)だけで、AWS側でバージョニング設定が完全に行き渡る前述の「15分」までは面倒を見てくれません。重要なオブジェクトを扱うなら、有効化と投入を別々の apply に分ける、もしくは15分空けるなどの配慮をしておくと確実です。ハンズオンの範囲では depends_on で十分実用になります。
コスト管理のライフサイクルも Terraform で書けます。「過去版は30日で失効、最新3世代は残す」はこう書きます。
resource "aws_s3_bucket_lifecycle_configuration" "site" {
bucket = aws_s3_bucket.site.id
# ライフサイクルはバージョニングが有効になってから
depends_on = [aws_s3_bucket_versioning.site]
rule {
id = "expire-noncurrent"
status = "Enabled"
filter {} # バケット全体を対象にする
noncurrent_version_expiration {
noncurrent_days = 30
newer_noncurrent_versions = 3
}
}
}
versioning_configuration.status には Disabled という値もありますが、これは「もともとバージョニングを使っていないバケット」をTerraform管理下に取り込む(import する)ための値です。Enabled から Disabled に戻すことはできません。AWSのAPIが未設定状態への復帰をサポートしておらず、エラーになります。停止したいときは Suspended を指定します。
まとめ
S3のバージョニングは「有効化」自体は一瞬ですが、そのあとの挙動を知らないとハマります。振り返ると、要点はこの3つでした。
- 状態は3つ(未設定 → 有効 → 停止)。有効化したら未設定には戻せない
- 削除は「削除マーカーを乗せるだけ」。復元しやすいが、放置すると過去版が課金され続ける
- だからライフサイクル(古い版の失効+残す世代数)で自動整理するところまでがワンセット
自分はここまでハンズオンで確認して、「バージョニングは“入れて終わり”じゃなくて“整理まで含めて設計するもの”なんだ」と腹落ちしました。次は削除マーカーからの復元や、Glacierへの transition を実際に試して、挙動を目で見てみたいと思っています。
もしあなたがすでにバージョニング有効のバケットを運用しているなら、一度ストレージの内訳(過去版がどれくらい溜まっているか)を覗いてみると面白いかもしれません。「え、こんなに?」となったら、それがライフサイクルの出番です。


