4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CloudFormationで構築するS3イミュータブルバックアップ

Last updated at Posted at 2025-12-12

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 配下
    • オブジェクトロックを設定しない(比較用)

01.jpg
図1: プレフィックスごとのオブジェクトライフサイクル設定

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バケット → 対象オブジェクト → プロパティから確認することも可能です。
02.JPG
図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における「ファイルをゴミ箱に入れた」ような状態です。(ゴミ箱の中にファイルはあるので復元可能)
03.jpg
図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
}

この状態で数日待つと、ライフサイクルルールにより以下の順序でオブジェクトが自動処理されることを確認しました。

  1. ExpirationInDays により削除マーカーが追加されます(図3の状態)
  2. NoncurrentVersionExpiration により非現行バージョンが完全削除され、削除マーカーのみになります
    04.jpg
    図4: 非現行バージョン削除後の DataA/file01 の状態(AWSマネジメントコンソール)
  3. 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に保存したデータが保護できランサムウェア対策の一つになることが期待できます。

ただし、オブジェクトロックはあくまでも削除・上書きに対する保護機能です。ランサムウェア対策としてはネットワーク制限や権限設計、監査ログなど他の対策と組み合わせて検討することが重要です。

参考

S3 Object Lock を使用したオブジェクトのロック

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?