CloudFormationで構築するS3イミュータブルバックアップ
はじめに
近年増加しているランサムウェアでは、バックアップデータを先に削除・暗号化してからデータ本体を暗号化する手口が確認されています。
そのため、バックアップを改ざん・削除できないようにする仕組みが対策の一つとして有効です。
Amazon S3にはオブジェクトロック機能があり、イミュータブルバックアップ、すなわち削除も上書きもできない状態でデータを保持することが可能です。
ただし、バケットレベルのデフォルト保持だけではプレフィックス(フォルダのような概念)ごとに保持期間を変えることはできません。
本記事では、次のような要件を満たすAmazon S3バケットをCloudFormationとLambdaを使って構築する方法を紹介します。
- 複数種のバックアップデータをS3バケットにプレフィックスを分けて保存する
- プレフィックスごとに異なる保持期間を定める
- 保持期間中は誰もがそのバックアップデータを削除・上書きできない
- 保持期間が過ぎたらS3から自動削除を行う
注意: 本記事に記載されているAWSリソース名・ID等はすべて例示用のダミーです
前提条件
- AWS CLIまたはマネジメントコンソールが利用可能
- CloudFormationスタックを作成する権限
- S3、Lambda、IAMの基本的な知識
Amazon S3の利用メリット
専用のバックアップ機器と比較するとAmazon S3には次のような利点があります。
- 利用量に応じた課金モデルで初期費用が不要
- 必要に応じて容量を柔軟に拡張できる
- 高い可用性と耐久性によりデータを安全に保管
- 物理的な設置場所の確保が不要
- ハードウェアの保守・交換が不要なので運用コストを軽減
データを保護するオブジェクトロック
S3オブジェクトロックは、オブジェクトを一定期間「削除・上書きできない」ように保護する機能で法令対応やランサムウェア対策に向いています。
S3オブジェクトロックには次の2つのモードがあります。
ガバナンスモード:s3:BypassGovernanceRetention権限を持つユーザが解除可能
コンプライアンスモード:いかなるユーザも保持期間中は削除・上書き不可
本記事のサンプル構成ではより強固な保護を行うため コンプライアンスモード を使用します。
CloudFormationでS3イミュータブル環境を構築する
サンプルとして次のように設定します。
- S3バケット
- オブジェクトロック(コンプライアンスモード)を有効化
- プレフィックス DataA 配下
- オブジェクトロック保持期間:1日
- オブジェクト作成から 2日目以降:削除マーカーを追加(ライフサイクル)
- オブジェクト作成から 3日目以降:非現行バージョンを完全削除(ライフサイクル)
- プレフィックス DataB 配下
- オブジェクトロック保持期間:2日
- オブジェクト作成から 3日目以降:削除マーカーを追加
- オブジェクト作成から 4日目以降:非現行バージョンを完全削除
- プレフィックス DataC 配下
- オブジェクトロックを設定しない(比較用)
S3バケットでオブジェクトロックを有効化する際の注意点
- ObjectLockEnabled: true は バケット作成時のみ指定可能
- オブジェクトロックはバージョニング設定が必須
本記事のサンプル構成では、さらに以下の仕組みを組み合わせています。
- アップロードイベントをトリガーに、Lambdaがプレフィックスに応じてオブジェクトに保持期間(コンプライアンスモード)を設定
サンプルテンプレートでは以下をまとめて作成します。
- KMSキー(S3暗号化用)
- オブジェクトロック有効なS3バケット(プレフィックス別ライフサイクル付き)
- オブジェクトロック保持期間を設定するLambda関数とそのIAMロール
- S3 → Lambdaのイベント通知設定
- バケットポリシー(TLS / SSE-KMSを強制するサンプル)
サンプルテンプレート(YAML)
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
# DataA
DataA_RetainDays:
Type: Number
Default: 1 # オブジェクトロック保持期間
Description: Object Lock retention (days, COMPLIANCE mode)
DataA_DeleteMarkerDays:
Type: Number
Default: 2 # 削除マーカーを付与するまでの日数
Description: Days after upload to add delete marker
DataA_NonCurrentDeleteDays:
Type: Number
Default: 1 # 非現行バージョンを削除するまでの日数
Description: Days after object becomes noncurrent to permanently delete
# DataB
DataB_RetainDays:
Type: Number
Default: 2 # オブジェクトロック保持期間
Description: Object Lock retention (days, COMPLIANCE mode)
DataB_DeleteMarkerDays:
Type: Number
Default: 3 # 削除マーカーを付与するまでの日数
Description: Days after upload to add delete marker
DataB_NonCurrentDeleteDays:
Type: Number
Default: 1 # 非現行バージョンを削除するまでの日数
Description: Days after object becomes noncurrent to permanently delete
Resources:
# KMSキー (S3バケットの暗号化に使用)
KmsKey:
Type: AWS::KMS::Key
Properties:
Description: Key for S3 Immutable Backup encryption
EnableKeyRotation: true
KeyPolicy:
Version: "2012-10-17"
Statement:
- Sid: Enable IAM User Permissions
Effect: Allow
Principal:
AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
Action: "kms:*"
Resource: "*"
- Sid: Allow use of the key by S3
Effect: Allow
Principal:
Service: s3.amazonaws.com
Action:
- kms:Encrypt
- kms:Decrypt
- kms:ReEncrypt*
- kms:GenerateDataKey*
- kms:DescribeKey
Resource: "*"
Condition:
StringEquals:
kms:ViaService: !Sub s3.${AWS::Region}.amazonaws.com
# KMSエイリアス(任意)
KmsKeyAlias:
Type: AWS::KMS::Alias
Properties:
AliasName: alias/immutable-backup-example-key
TargetKeyId: !Ref KmsKey
ImmutableBackupLambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowLambdaAssume
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: LambdaRetentionPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowPutRetention
Effect: Allow
Action:
- s3:PutObjectRetention
- s3:GetObject
Resource: !Sub arn:aws:s3:::${ImmutableBackupBucket}/*
- Sid: AllowLambdaLogWrite
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "*"
# Lambda
S3ObjectLockLambda:
Type: AWS::Lambda::Function
Properties:
FunctionName: set-retention-example
Handler: index.handler
Role: !GetAtt ImmutableBackupLambdaRole.Arn
Runtime: python3.12
Timeout: 10
Environment:
Variables:
DATAA_RETENTION_DAYS: !Ref DataA_RetainDays
DATAB_RETENTION_DAYS: !Ref DataB_RetainDays
Code:
ZipFile: |
import boto3
import os
import datetime
def handler(event, context):
s3 = boto3.client('s3')
for record in event['Records']:
bucket = record['s3']['bucket']['name']
key = record['s3']['object']['key']
if key.startswith('DataA/'):
retention_days = int(os.environ.get('DATAA_RETENTION_DAYS', 1))
elif key.startswith('DataB/'):
retention_days = int(os.environ.get('DATAB_RETENTION_DAYS', 1))
else:
retention_days = 1
s3.put_object_retention(
Bucket=bucket,
Key=key,
Retention={
'Mode': 'COMPLIANCE',
'RetainUntilDate': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=retention_days)
},
BypassGovernanceRetention=False
)
# S3がLambdaを呼び出すための許可
ImmutableBackupLambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt S3ObjectLockLambda.Arn
Action: lambda:InvokeFunction
Principal: s3.amazonaws.com
SourceArn: !Sub arn:aws:s3:::${ImmutableBackupBucket}
ImmutableBackupBucket:
Type: AWS::S3::Bucket
Properties:
# S3バケット名はグローバルで一意である必要があるためご自身の環境に合わせた一意の名前に変更してください
BucketName: immutable-backup-example
ObjectLockEnabled: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: aws:kms
KMSMasterKeyID: !Ref KmsKey
VersioningConfiguration:
Status: Enabled
LifecycleConfiguration:
Rules:
- Id: DataACleanup
Prefix: DataA/
Status: Enabled
ExpirationInDays: !Ref DataA_DeleteMarkerDays
NoncurrentVersionExpiration:
NoncurrentDays: !Ref DataA_NonCurrentDeleteDays
- Id: DataACleanupDeleteMarker
Prefix: DataA/
Status: Enabled
ExpiredObjectDeleteMarker: true
- Id: DataBCleanup
Prefix: DataB/
Status: Enabled
ExpirationInDays: !Ref DataB_DeleteMarkerDays
NoncurrentVersionExpiration:
NoncurrentDays: !Ref DataB_NonCurrentDeleteDays
- Id: DataBCleanupDeleteMarker
Prefix: DataB/
Status: Enabled
ExpiredObjectDeleteMarker: true
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
# ※下記NotificationConfigurationは他のリソース作成後に実施します (もしくはDependsOnを使う)
# S3バケット通知設定(DataA/とDataB/ にマッチするオブジェクトが作成された場合にLambdaへ通知)
NotificationConfiguration:
LambdaConfigurations:
- Event: "s3:ObjectCreated:*"
Filter:
S3Key:
Rules:
- Name: prefix
Value: DataA/
Function: !GetAtt S3ObjectLockLambda.Arn
- Event: "s3:ObjectCreated:*"
Filter:
S3Key:
Rules:
- Name: prefix
Value: DataB/
Function: !GetAtt S3ObjectLockLambda.Arn
# 本番環境ではPrincipal/Actionは最小権限にしてください
ImmutableBackupBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref ImmutableBackupBucket
PolicyDocument:
Version: "2012-10-17"
# 検証用に暫定的にAllowを用いていますがセキュリティの観点からDenyポリシーが推奨となります
Statement:
- Sid: AllowListOverTLS
Effect: Allow
Principal:
AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
# AWS: "*" # 必要に応じ適切なプリンシパルを指定します
Action:
- s3:ListBucket
- s3:ListBucketVersions
Resource: !Sub "arn:aws:s3:::${ImmutableBackupBucket}"
Condition:
Bool:
aws:SecureTransport: true
- Sid: AllowGetOverTLS
Effect: Allow
Principal:
AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
# AWS: "*" # 必要に応じ適切なプリンシパルを指定します
Action:
- s3:GetObject
- s3:GetObjectVersion
- s3:GetObjectRetention
Resource: !Sub "arn:aws:s3:::${ImmutableBackupBucket}/*"
Condition:
Bool:
aws:SecureTransport: true
- Sid: AllowPutWithKmsAndTLS
Effect: Allow
Principal:
AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
# AWS: "*" # 必要に応じ適切なプリンシパルを指定します
Action:
- s3:PutObject
Resource: !Sub "arn:aws:s3:::${ImmutableBackupBucket}/*"
Condition:
Bool:
aws:SecureTransport: true
StringEquals:
s3:x-amz-server-side-encryption-aws-kms-key-id: !Ref KmsKey
削除できないことを確認してみる
テストデータのアップロード
$ aws s3 cp ./file01 s3://immutable-backup-example/DataA/ --sse aws:kms --sse-kms-key-id alias/immutable-backup-example-key
upload: ./file01 to s3://immutable-backup-example/DataA/file01
$ aws s3 cp ./file02 s3://immutable-backup-example/DataB/ --sse aws:kms --sse-kms-key-id alias/immutable-backup-example-key
upload: ./file02 to s3://immutable-backup-example/DataB/file02
$ aws s3 cp ./file03 s3://immutable-backup-example/DataC/ --sse aws:kms --sse-kms-key-id alias/immutable-backup-example-key
upload: ./file03 to s3://immutable-backup-example/DataC/file03
オブジェクトロック設定の確認
$ aws s3api get-object-retention --bucket immutable-backup-example --key DataA/file01
{
"Retention": {
"Mode": "COMPLIANCE",
"RetainUntilDate": "2025-11-22T04:35:37.374Z"
}
}
$ aws s3api get-object-retention --bucket immutable-backup-example --key DataB/file02
{
"Retention": {
"Mode": "COMPLIANCE",
"RetainUntilDate": "2025-11-23T04:35:42.454Z"
}
}
$ aws s3api get-object-retention --bucket immutable-backup-example --key DataC/file03
An error occurred (NoSuchObjectLockConfiguration) when calling the GetObjectRetention operation: The specified object does not have a ObjectLock configuration
上記の出力から以下のことが分かります。
"Mode": "COMPLIANCE" → コンプライアンスモードでロック中
"RetainUntilDate" → この日時までは削除・上書きできない(実際の値は環境・実行タイミングによって変わります)
AWSマネジメントコンソール → S3バケット → 対象オブジェクト → プロパティから確認することも可能です。

図2: オブジェクトロックの確認例(AWSマネジメントコンソール)
削除マーカーの追加テスト(論理削除の挙動)
# 削除マーカーの追加を行います
$ aws s3 rm s3://immutable-backup-example/DataA/file01
delete: s3://immutable-backup-example/DataA/file01
$ aws s3 rm s3://immutable-backup-example/DataB/file02
delete: s3://immutable-backup-example/DataB/file02
$ aws s3 rm s3://immutable-backup-example/DataC/file03
delete: s3://immutable-backup-example/DataC/file03
# バージョンIDを確認します
$ aws s3api list-object-versions --bucket immutable-backup-example --prefix DataA/file01
{
"DeleteMarkers": [
{
"Owner": {
"ID": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"IsLatest": true,
"VersionId": "bST9jqXGOzQ8vl7NArjob60pIUKW2565",
"Key": "DataA/file01",
"LastModified": "2025-11-21T04:36:56.000Z"
}
],
"Versions": [
{
"LastModified": "2025-11-21T04:35:37.000Z",
"VersionId": "Vh5UhZyR3h98SRqmaCoj0h4Kx8lb8eya",
"ETag": "\"468c29f19ae7ff04c9a114413ef95120\"",
"StorageClass": "STANDARD",
"Key": "DataA/file01",
"Owner": {
"ID": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"IsLatest": false,
"Size": 14
}
]
}
$ aws s3api list-object-versions --bucket immutable-backup-example --prefix DataB/file02
{
"DeleteMarkers": [
{
"Owner": {
"ID": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"IsLatest": true,
"VersionId": "GOpEL07EKmgjI1zhLZf95WmU3kTfSnQ7",
"Key": "DataB/file02",
"LastModified": "2025-11-21T04:37:00.000Z"
}
],
"Versions": [
{
"LastModified": "2025-11-21T04:35:42.000Z",
"VersionId": "sk4t_OO2NX7_ZNaNeOn1OR55U7nTyfmw",
"ETag": "\"34e17891f16c92eca91f87b150d52ba4\"",
"StorageClass": "STANDARD",
"Key": "DataB/file02",
"Owner": {
"ID": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"IsLatest": false,
"Size": 14
}
]
}
$ aws s3api list-object-versions --bucket immutable-backup-example --prefix DataC/file03
{
"DeleteMarkers": [
{
"Owner": {
"ID": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"IsLatest": true,
"VersionId": "ehVFNEXnAi0UTLVbIVrLPw8wSL45rbI5",
"Key": "DataC/file03",
"LastModified": "2025-11-21T04:37:04.000Z"
}
],
"Versions": [
{
"LastModified": "2025-11-21T04:36:18.000Z",
"VersionId": "t3Oy548.2MbbYTP8GAIdI7QdHRnBh81E",
"ETag": "\"4bd5680f590e0e4ff624e19b8f240e8f\"",
"StorageClass": "STANDARD",
"Key": "DataC/file03",
"Owner": {
"ID": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
"IsLatest": false,
"Size": 14
}
]
}
コンプライアンスモードでロック中のオブジェクトも削除(正確には削除マーカーの追加)することはできました。
削除マーカーの追加は、例えるならWindowsにおける「ファイルをゴミ箱に入れた」ような状態です。(ゴミ箱の中にファイルはあるので復元可能)

図3: 削除マーカー追加後の DataA/file01 の状態(AWSマネジメントコンソール)
物理削除(完全削除)ができないことを確認
# コンプライアンスモード保持期間内のオブジェクトの削除を試みます
$ aws s3api delete-object --bucket immutable-backup-example --key DataA/file01 --version-id "Vh5UhZyR3h98SRqmaCoj0h4Kx8lb8eya"
An error occurred (AccessDenied) when calling the DeleteObject operation: Access Denied because object protected by object lock.
$ aws s3api delete-object --bucket immutable-backup-example --key DataB/file02 --version-id "sk4t_OO2NX7_ZNaNeOn1OR55U7nTyfmw"
An error occurred (AccessDenied) when calling the DeleteObject operation: Access Denied because object protected by object lock.
$ aws s3api delete-object --bucket immutable-backup-example --key DataC/file03 --version-id "t3Oy548.2MbbYTP8GAIdI7QdHRnBh81E"
{
"VersionId": "t3Oy548.2MbbYTP8GAIdI7QdHRnBh81E"
}
オブジェクトの物理削除を試みますが、コンプライアンスモードによってその保持期間内は物理削除できないことを確認しました。
ただし、DataC配下はオブジェクトロックが設定されていないので完全削除ができました。(比較用のテストケース)
ライフサイクルルールのテスト(自動削除の確認)
次に、自動削除を確認するため前項で実施した削除マーカーを取り除いておきます。(その結果、元のバージョンが復活します)
なお、ライフサイクルによる削除はオブジェクトロックの保持期限が過ぎたオブジェクトに対してのみ有効であり、保持期間内のバージョンはルールが存在していても削除されません。
# 削除マーカーセクションにあるVersionIdを指定して削除マーカーを取り除きます
$ aws s3api delete-object --bucket immutable-backup-example --key DataA/file01 --version-id "bST9jqXGOzQ8vl7NArjob60pIUKW2565"
{
"VersionId": "bST9jqXGOzQ8vl7NArjob60pIUKW2565",
"DeleteMarker": true
}
$ aws s3api delete-object --bucket immutable-backup-example --key DataB/file02 --version-id "GOpEL07EKmgjI1zhLZf95WmU3kTfSnQ7"
{
"VersionId": "GOpEL07EKmgjI1zhLZf95WmU3kTfSnQ7",
"DeleteMarker": true
}
この状態で数日待つと、ライフサイクルルールにより以下の順序でオブジェクトが自動処理されることを確認しました。
- ExpirationInDays により削除マーカーが追加されます(図3の状態)
-
NoncurrentVersionExpiration により非現行バージョンが完全削除され、削除マーカーのみになります

図4: 非現行バージョン削除後の DataA/file01 の状態(AWSマネジメントコンソール) - ExpiredObjectDeleteMarker により孤立した削除マーカーが削除されます
注意: ライフサイクルルールは1日に1回程度の頻度で評価されます。実際に条件を満たしてから処理が行われるまで、最大で24〜48時間程度のラグが発生する場合があります。
運用上の注意点
ここで挙げる内容は一例ですが、必要に応じ設計・運用時の参考にしてください。
セキュリティ関連
- 保存するデータの暗号化を行うかどうか、および採用する方式(SSE-KMSなど) を明確にする
- S3への通信経路 が安全であることを確認
- パブリックアクセスを意図せず許可しないよう、ブロック設定やバケットポリシーを適切に構成
コンプライアンス関連
- 機密性の高いデータや個人情報を日本国外リージョンに保存する場合は、関連する法制度・契約・運用体制を事前に確認しておくこと
- 保持ポリシーが法的要件(例:電子帳簿保存法など)に適合しているかも併せて検討
コスト関連
コストの中心は S3の保存料金 で、他にLambda実行費用などが発生します。
料金は以下の要因によって変動します。
- 保存データの容量
- 保持期間
- 利用リージョン
- 為替レート など
コンプライアンスモードでは保持期間を過ぎるまで削除できないため、運用開始前にコスト試算と設計レビューを行うことをお勧めします。
Lambda遅延に関する注意
本記事のLambdaを用いた構成では、イベント駆動がPUT後に実行されるため、保持設定が反映されるまでに数百ミリ秒〜数秒程度の遅延が発生します。
この遅延を回避したい場合は以下の方法を検討してください。
- バケットを用途別に分けて構築しデフォルト保持設定で制御する
- クライアント側でアップロード時に
x-amz-object-lock-mode/x-amz-object-lock-retain-until-dateヘッダーをプレフィックスに応じて指定する
まとめ
Amazon S3のプレフィックスごとに異なるオブジェクトロック保持期間を指定する設定をCloudFormationとLambdaを用いてご紹介しました。
これによりS3に保存したデータが保護できランサムウェア対策の一つになることが期待できます。
ただし、オブジェクトロックはあくまでも削除・上書きに対する保護機能です。ランサムウェア対策としてはネットワーク制限や権限設計、監査ログなど他の対策と組み合わせて検討することが重要です。
