spacelyのブログ

Spacely Engineer's Blog

AWS Backup for S3なしにS3をバックアップする

インフラエンジニアの thaim です。

つい先日、待望のAWS Backup for S3がリリースされました!

aws.amazon.com

ちょうど利用したい案件があったのですが、スケジュールの都合上GAを待たずバックアップを取得する必要がありました。 そのため、AWS Backup for S3なしにS3のバックアップを実現する方法についていろいろと検討・実施していました。 AWS Backup for S3があればもう不要かもしれませんが、供養のためここにまとめておきます。

S3バケットをバックアップしたい

S3バケットの設定はTerraformなどでコード化してバックアップできますが、S3に保存しているオブジェクトは当然対象外でした。 アプリやアーキテクチャの大規模改修前、S3のライフサイクルの設定変更といったイベントの前にはS3のオブジェクトをバックアップしておきたくなります。

もちろん、バージョニング機能を利用すればオブジェクトの意図しない変更が生じても元に戻すことはできます。 しかし、意図しないライフサイクルの動作に備えるためにはバージョニング機能だけでは十分ではありません。 あくまで、別バケットにオブジェクトをコピーしてバックアップする必要があります。 (これはストレージのバックアップにスナップショットだけでは不十分なのと同じですね)。

また、運用しているサービスを止めることなくS3バケットのバックアップを実現する必要がありました。 サービスが稼動している中で、随時S3バケットにオブジェクトの作成・更新・削除が行われます。 このため、特定のタイミングにおけるバックアップを取得するだけでは十分ではなく、 新しく作成するオブジェクトまたは既存オブジェクトを更新する内容についても考慮する必要があります。

これらを踏まえて、以下のような要件でバックアップを実現したいと考えました。

  • S3の設定ではなくオブジェクトに関するバックアップを実現する
  • 意図しないライフサクルの動作に備えたバックアップを実現する
  • 継続的にオブジェクトが更新される環境下で静止点なしにバックアップを実現する

S3バックアップ方針

バックアップの実現方法としては以下の選択肢が挙げられます。

aws cli等によるコピー

単にaws cliコマンドで aws s3 cp s3://source s3://dest と実行してコピーを作成する方法です。 また、必要に応じてcpコマンドではなくsyncコマンドを利用する方法も考えられます。 この方法では簡単にコピーを実行することができます。

一方でオブジェクト数が多くなると実行に時間が掛かるか、タイムアウトが発生するなどして上手くいかないという課題があります。 また、継続的なバックアップを行う場合はどのようにaws cliコマンドを実行するか検討する必要もありました。 AWSエンジニアの方に相談したところオブジェクト数が1000万あたりに上限があるとのことで、 今回は対象オブジェクト数が1億以上あるためこの方式は断念となりました。

レプリケーションルール

レプリケーションルールを利用すればオブジェクトをそのまま別のS3バケットにコピー(レプリケーション)してくれます。 これにより新規に作成したオブジェクトをもれなく別バケットにコピーできます。 また、削除マーカーはレプリケーションしない設定が可能なので、意図しないオブジェクトの削除が発生してもバックアップ先には反映されず、継続的なバックアップとしても利用できます。 レプリケート先ではバージョニング機能が動作するので、意図しない削除だけでなく意図しない変更にも対応できます。

しかし、レプリケートルールは新規変更のみが対象で、既存のオブジェクトに対してはレプリケーションは適用できないという課題があります。 デフォルトでは機能が無効化されており、既存のオブジェクトに対してレプリケーションを適応したい場合はAWSサポートを通して機能の有効化リクエストが必要になります。 これで対処できるなら一番わかりやすいが、既存のオブジェクトに対するレプリケート機能を有効化するには、AWSサポートに申請してから有効化が反映されるまで1ヶ月以上掛かることがわかりました。 今回のケースでは検証環境含めた環境構築のリード時間に1ヶ月以上は許容されないので、この方式は諦めました。

バッチオペレーション

バッチオペレーションを利用すればcliでは対応できないような大量のオブジェクトに対してコピー操作などが適用できます。 また、これを利用することでレプリケートルールでは対応が難しい既存のオブジェクトに対するコピーが可能となります。

ただし、バッチオペレーションは実行した時点でのコピーしか取得できないという課題があります。 このためオブジェクトが更新され続けるバケットの継続的なバックアップとしては不十分です。 継続的にバッチオペレーションを実行することで対応することも考えられますが、 バッチオペレーション実行タイミングが静止点となるので、やはり継続的なバックアップとしては不十分と判断しました。

AWS Backup for S3

AWS Backup for S3AWS re:Invent 2021で発表されたプレビュー機能です。 これを利用することで、既存オブジェクトのフルバックアップに加えて継続的なポイントインタイムバックアップが可能となります。また、復元操作も簡単になるのも嬉しところです。

残念ながら、一般公開されたのが 2022-02-18であり、 自分たちがS3バックアップを検討していた頃はまだGA前だったため、詳細な機能説明もなかったため実現方式から除外しました。

バッチオペレーション+レプリケーションルール

以上の方針検討結果を踏まえて、バッチオペレーションとレプリケーションルールを組合せて利用することに決めました。

既存のオブジェクトはバッチオペレーションでバックアップし、継続的な作成・更新はレプリケーションルールでバックアップします。 また、オブジェクトの削除は削除マーカーを対象外にすることでバックアップ先への反映を除外します。 これにより既存オブジェクトのフルバックアップと差分の継続的なバックアップを実現できます。

バックアップの実施

バックアップの実施には大きく2つのステップで構成します。 1stステップはバックアップの準備で、主にTerraformを用いて実行します。 2ndステップはバックアップの実行で、主に手動操作で実行します。

バックアップに必要なリソースの構築(terraform)

まず始めにTerraformでバックアップに必要なリソースを作成します。 具体的には以下の通りです(terraformコードは重要な部分のみ抜粋)。

バックアップ先S3バケット

オブジェクトのコピーを保存するS3バケット。今回はバックアップ対象と同じAWSアカウント・リージョンに作成します。 バケットのプロパティやアクセス許可設定、特にパブリックアクセスブロックはバックアップ元と同じ条件とする必要がある点に注意が必要です(詳細後述)。

# バックアップ先のS3バケット
resource "aws_s3_bucket" "backup_dest" {
  bucket = "backup-dest"

  versioning {
    enabled = true
  }
}

resource "aws_s3_bucket_public_access_block" "backup_dest" {
  bucket = aws_s3_bucket.backup_dest.bucket

  # レプリケート元と同じACLにしないとレプリケートに失敗する
  block_public_acls       = false
  ignore_public_acls      = true
  block_public_policy     = true
  restrict_public_buckets = true
}

既存オブジェクトのバッチオペレーション用インベントリ設定

既存オブジェクトのバックアップはバッチオペレーションで実現しますが、バッチオペレーションが対象とするオブジェクト一覧を提供する必要があります。 このオブジェクト一覧をインベントリ設定を用いて生成します。 ここでは、インベントリを1日1回の頻度でバックアップ先と同じバケットに保存します。

resource "aws_s3_bucket_inventory" "buckup_src" {
  bucket = aws_s3_bucket.backup_src.id
  name   = "ExistingCurrentObjectForBackup"

  included_object_versions = "Current"
  schedule {
    frequency = "Daily"
  }
  destination {
    bucket {
      bucket_arn = aws_s3_bucket.backup_dest.arn
      format     = "CSV"
    }
  }
}

resource "aws_iam_policy" "grant_access_s3_for_backup_batch_operations" {
  name        = "S3AccessBackupBatchOperations"
  path        = "/"
  description = "Allow Access to S3 for Backup by Batch Operations"

  policy = <<POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowAccessDestinationBucket",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl",
                "s3:PutObjectTagging",
                "s3:PutObjectLegalHold",
                "s3:PutObjectRetention",
                "s3:GetBucketObjectLockConfiguration"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::${local.s3_bucket_backup_dest}/*"
        },
        {
            "Sid": "AllowAccessSourceBucket",
            "Action": [
                "s3:GetObject",
                "s3:GetObjectAcl",
                "s3:GetObjectTagging"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::${local.s3_bucket_backup_src}/*"
        },
        {
            "Sid": "AllowAccessInventoryConfiguration",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:GetBucketLocation"
            ],
            "Resource": [
                "arn:aws:s3:::${local.s3_bucket_backup_dest}/${local.s3_bucket_backup_src}/*"
            ]
        }
    ]
}
POLICY
}

新規オブジェクト用のレプリケーション設定

バックアップ元のS3バケットに作成・更新したオブジェクトを、バックアップ先のバケットにコピーするレプリケーション設定を作成します。 この設定を作成した時点でバックアップの一部動作が開始しますが、既存オブジェクトのバックアップを実行した時点でレプリケーションは有効化しておく必要があります。 このため、この準備ステップにおいてレプリケーションだけは先行して有効にしておきます。

# バックアップ元バケットに対するレプリケーション設定
resource "aws_s3_bucket_replication_configuration" "backup_src" {
  bucket = aws_s3_bucket.backup_src.id
  role   = aws_iam_role.backup_src_bucket_replication_configuration.arn

  rule {
    id     = "backup_newobjects"
    status = "Enabled"

    source_selection_criteria {
      sse_kms_encrypted_objects {
        status = "Enabled"
      }
    }

    destination {
      bucket = aws_s3_bucket.backup_dest.arn
      encryption_configuration {
        replica_kms_key_id = data.aws_kms_alias.s3_default.arn
      }
    }

    # delete_marker_replicationを指定するためには Replication Configuration v2を利用する必要がある
    # v2利用ではfilterの指定が必須なので空で定義する
    filter {
    }

    # バックアップ目的なのでdelete markerはレプリケートしない
    delete_marker_replication {
      status = "Disabled"
    }
  }
}

# replication configurationで利用するポリシー
# aws_iam_role.backup_src_bucket_replication_configuration 自体は記載省略
resource "aws_iam_policy" "grant_access_s3_for_backup_replication" {
  name        = "S3AccessBackupReplication"
  path        = "/"
  description = "Allow Access to S3 for Backup by Replication Configuration"

  policy = <<POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:ListBucket",
                "s3:GetReplicationConfiguration",
                "s3:GetObjectVersionForReplication",
                "s3:GetObjectVersionAcl",
                "s3:GetObjectVersionTagging",
                "s3:GetObjectRetention",
                "s3:GetObjectLegalHold"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::${local.s3_bucket_src}",
                "arn:aws:s3:::${local.s3_bucket_src}/*",
                "arn:aws:s3:::${local.s3_bucket_dest}",
                "arn:aws:s3:::${local.s3_bucket_dest}/*"
            ]
        },
        {
            "Action": [
                "s3:ReplicateObject",
                "s3:ReplicateDelete",
                "s3:ReplicateTags",
                "s3:ObjectOwnerOverrideToBucketOwner"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::${local.s3_bucket_src}/*",
                "arn:aws:s3:::${local.s3_bucket_dest}/*"
            ]
        }
    ]
}
POLICY
}

バックアップの実行(手動運用)

前述のterraformコードによりバックアップの準備が完了すると、バッチオペレーションによる既存オブジェクトのバックアップの実行準備が整います。 ただし、インベントリの出力は初回は最大48時間、以降は24時間毎に実行される点に注意が必要です。 このため、Terraformによるバックアップの準備を行った後、バックアップを実行するまでに最大2日待つ必要があります。

インベントリは指定したバケット<対象バケット>/<インベントリ設定名>/date/<uuid>.csv.gz というファイル名でイベントリファイルを出力します。 また、 <対象バケット>/<インベントリ設定名>/<YYYY-MM-dd'T'HH-mm'Z'>/manifest.json に出力したイベントリファイルのメタ情報が格納されます。 このインベントリを利用してバッチオペレーションを実行します。 バッチオペレーションでは以下の3ステップで実行します。

  • インベントリファイルを入力としたジョブの作成
  • ジョブのインベントリファイルロード
  • ロード結果の確認と手動承認

f:id:thaim:20220301122925p:plain
バッチオペレーションの承認待ち画面
f:id:thaim:20220301123035p:plain
バッチオペレーションの実行完了画面

バックアップ対象のオブジェクト数が多いと、ジョブのロードと手動承認後のバッチオペレーション実行には時間が掛かりますが、 この操作により既存のオブジェクトがバッチオペレーションによりバックアップ先にコピーされます。 このバッチオペレーションとバックアップ準備の中で実行したレプリケーションを合わせてバックアップの完了です。

ハマりどころ

レプリケーションやバッチオペレーションは普段利用する機会が少なく、いろいろと手間取ってしまいました。 以下は特に時間を掛けてしまったところです。

既存オブジェクトのレプリケート機能の有効化手順

既存のオブジェクトのレプリケーション機能を有効化するためにはAWSサポートへの問合せが必要とは理解していましたが、 この問合せフォーマットがあることを把握していませんでした。 このため、やりとりに余計な手間が生じてしまいました。タイトルおよびレプリケーションに関する情報をきちんと記載する必要がありました。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/replication-what-is-isnot-replicated.html#existing-object-replication

また、この有効化に1ヶ月以上掛かることも想定していませんでした。 当初は既存オブジェクトも新規オブジェクトもレプリケートルールでバックアップすることを想定していましたが、 準備に1ヶ月掛かることを把握しておらず、スケジュールの関係からこの方式は断念しました。

ブロックパブリックアクセス

レプリケーションでファイルを別バケットにコピーする場合、ブロックパブリックアクセスの設定が影響します。 レプリケート元のACLによるアクセスを許可している場合、レプリケート先もACLによるアクセスを許可する必要があります。 基本的にはすべてブロックする設定が望ましいですが、何らかの理由によりレプリケート元のACLアクセスが許可されている場合に バックアップ先だからとブロック設定にすると上手く動作しないので注意が必要です。

今回バックアップしたいS3バケットはブロックパブリックアクセスを許可していたので、バックアップ先のブロックパブリックアクセスも許可する必要がありました。 多くのケースではブロックパブリックアクセスは許可していないと思いますが、設定によっては注意が必要です。 (こういった特殊な設定環境にあるからこそバックアップを取得したくなったのですが)

参考: バケット間にレプリケーションを設定しましたが、新しいオブジェクトがレプリケートされません。この問題をトラブルシューティングするにはどうすればよいですか?

インベントリレポート出力のバケットポリシー許可

インベントリ設定においてレポートを出力する際の権限設定としてIAMロールは指定しませんが、レポート出力先のS3バケットポリシーは考慮されます。 このため、インベントリレポートの出力先のS3バケットポリシーにはファイルアップロードの許可を追加する必要があります。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/example-bucket-policies.html#example-bucket-policies-use-case-9

AWSマネージメントコンソールでインベントリ設定を行うと、自動で対象バケットにアクセス許可を設定してくれます。 しかし、Terraformでインベントリ設定を行うと自分でインベントリレポートの出力許可設定をバケットポリシーに設定する必要があります。 Terraformだけで検証していると気付けないので注意が必要です。

f:id:thaim:20220301124916p:plain
AWSマネージドコンソールが自動生成するバケットポリシー

Terraformによるレプリケーションルールの設定

Terraformにてレプリケーションルールを設定する場合、aws_s3_bucket_replication_configurationリソースを利用します。 今回のように削除マーカーをレプリケーションしない場合はレプリケーション設定v2を利用する必要があります。

Terraformでレプリケーション設定v2を利用するには明示的にバージョンを指定するのではなく、 filterルールを指定しているかどうかで自動判定されます。 今回はバケット内の全オブジェクトが対象なのでfilterルールは不要ですが、レプリケーション設定v2を利用するために空のfilterを指定します。

registry.terraform.io

バックアップファイルの削除

これはバックアップについてではなく後日談ですが。 バックアップの用途が終わり、バックアップファイルを削除しようと思っても簡単ではありませんでした。 バックアップの削除の手順としては以下のようになります:

特にバケットを空にする作業が面倒で、バックアップ容量がTBクラスになると削除も単にマネージメントコンソールから空にするを選択しても上手くいきません。 多数のオブジェクトが含まれている場合は削除に時間が掛かります。 親切なことに、マネージドコンソールではバケットを空にする操作を行う前にAWSからライフサイクルルールによる削除を推奨されます。 このドキュメントに従い、ライフサイクルルールを通してオブジェクトを削除するとスムーズに対応できます。

f:id:thaim:20220301125719p:plain
ライフサイクルによるオブジェクト削除の推奨メッセージ

また、ライフサイクルを通してバケットを空にする場合は1つのライフサイクルでは設定できません。 「オブジェクトの現在のバージョンの有効期限切れ」オプションと「期限切れのオブジェクト削除マーカーの削除」のオプションは同時に指定できないからです。 このため2つのライフサイクルルールを通してバケットを空にする設定を実現する必要があります。

f:id:thaim:20220301130014p:plain
ライフサイクルで複数オプションを指定できない

まとめ

  • レプリケーションルールやバッチオペレーションは頻繁に使う機能ではないので初めて詳細に検証した。いろいろ便利だが使いこなすのは大変。
  • 今回はバックアップ後に障害発生してリストアが必要だとしても個別対応でよいという判断だった。システマティックに対応できるためには静止点が必要でありレプリケーションルールを削除するなどの対応が必要だった。検知も含めて更に大変になりそう。
  • AWS Backup for S3がGAになったので今後はこのような作業は不要になるはず。

このあたりはまだいろいろと書けますが、ちょっと長くなってしまったのでここまでにします。 もっと詳しい話が気になる人は採用ページからカジュアル面談等でお待ちしています。