S3 Batch Operations によるクロスアカウントファイルコピーにおけるエラーとその対応

2020.11.16

はじめに

こむろ@札幌です。

S3 Batch Operations の Job を利用し、大量のデータを AWS アカウント間で転送する機会があったので、そちらで遭遇したエラーとその原因、解決方法について記載します。背景とコンテキストについての記述が結構な量あるので、あまり現場の前提やコンテキストに興味がない方は、背景や前提を飛ばしてください。

背景

現在、ある AWS アカウントで稼働させているアプリケーションのすべてのデータを別の AWS アカウント移行させるためのプロジェクトを担当しています。

すでに稼働から3年近く経過しているため、蓄積しているデータの数は非常に多く、転送にかかる時間を事前に計測した値から推測し、移行計画をきちんと立てないとうまくいかなそうな規模です。マスタデータである DynamoDB テーブルのアイテム数の多くは数千万、多いものでは数億ほど保持しています。

移行の対象となるデータは以下の通りです。

  • DynamoDB のテーブル・アイテムすべて
  • ElastiCache for Redis のメモリ内容すべて
  • S3 のオブジェクトすべて

それぞれのデータは以下のような関連を持ちます。雑な概念図です。

マスターデータの格納先として DynamoDB を主に利用します。サイズの関係でアイテムに入り切らないデータの一部を S3 にオブジェクトとして退避させており、 DynamoDB のアイテム内に S3 へのキーを保持することで関連をもたせています。また、処理速度向上及び DynamoDB のCapacity Unit 消費抑制のため、ElastiCache を利用して一部のデータを Cache しています。

Redis に関しては単なる Cache としているのが主たる機能ですので、最悪消えてしまってもマスタデータから再構築が可能です。(ただし、マスタデータ側へ負荷はかかるので注意は必要)

ここで厄介なのはDynamoDBのアイテムとS3のオブジェクトの関係です。DynamoDBのアイテムに従属する形でS3のオブジェクトが存在する形です。そのため、「DynamoDBのアイテムは存在するが、 S3 のオブジェクトが存在しない」という状況が生まれた場合、使用しているアプリケーションが予期せぬ動作をすることが予測できます。 *1

従ってDynamoDBのテーブルのアイテム数とS3のオブジェクト数を完全に一致させる必要があります。そしてよりにもよってこの厄介な関連性を持っているアイテムが、DynamoDB のテーブル内で最高の2億アイテムに該当します。これはなかなか大変だ。

データの特性について

2億件のデータ特性について軽く触れておきます。

詳細の記載は省きますが、大部分のデータはほとんどアクセスされず、利用もされません。従って、アクティブなデータはごく一部になります。アプリケーションを停止してしまえばデータは完全に静止状態を保つことができます。(DynamoDB, S3 共に TTLの設定等がなく、アプリケーションによって削除される以外の方法はない)

本来、データ移行する際には必要なアクティブなデータのみに絞って移行するのが効率的です。しかし、今回はアプリケーションの制約と移行までの準備・検証期間の短さから、これらいくつかの根本的な問題に目をつぶった上で、非効率ではありますが、本来不要なアイテムも含めてまるごとすべて移行をするという強引な手段を取っています。

S3 の移行に関する基本的な戦略

背景を一通り説明した上で、まずは S3 のオブジェクト移行について記載します。今回の移行作業は、アクセス頻度の少ない大部分を事前に移行した上で、移行当日はアプリケーションを停止して、データを静止状態にした後、事前以降との差分を同期する手法をとります。

事前の大部分の移行件数は 1 億件弱です。 S3 の AWS アカウント間でのデータ転送方法にはいくつか選択肢があります。

  • S3 sync を利用する
  • S3 Replication を利用する
  • S3 Batch Operations を利用する

いずれも実現可能です。ただ、Replication については既存のオブジェクトも転送対象にするためにはサポートへの連絡が必要なため、完全にこちら側でコントロールするには厳しい部分があります。今回は、S3 Batch Operations を利用して転送する方法を検証しました。

複数のS3のオブジェクトをまとめて一括処理!Amazon S3 Batch Operationsを試してみた

S3 Batch Operations を利用したオブジェクト転送の構成

今回対応するケースは AWS ドキュメントのクロスアカウントでの転送例と同じようなケースです。

ソースアカウントに保存された CSV マニフェストを使用して AWS アカウント間でオブジェクトをコピーする

S3 Inventory Report を転送元のアカウント配下の Bucket へ出力し、S3 Batch Operations の Job は転送先のAWSアカウント内で作成します。転送先から転送元の S3 Inventory Report の Manifest と転送元になるファイルに対してクロスアカウントでアクセスするという構成です。

構成は以下の通りです。

S3 Batch Operations をめぐるエラーメッセージ

ドキュメント通りいけば全く問題がなかったのですが (私の知識不足等もあり) 結果として、正常に転送できるまで数多のエラーに遭遇しました。エラーメッセージから原因がなかなかわからず、紆余曲折を経て解決したので記録として残しておきます。(ここからが本題です)

S3 Inventory Report, S3 Batch Operations の組み合わせをクロスアカウントで利用する場合は、ご参考いただけるかと思います。

マニフェストオブジェクトのETag を取得するためのアクセス許可が不十分

S3 Batch Operations で Job を作成する際にクロスアカウントでアクセスする Manifest になんらかの不備がある場合、このエラーメッセージが出力されます。以下、原因と対応です。

原因: Manifest ファイルが存在しない

S3 Inventory Report の Manifest がそもそも出力されていない Key を指定している場合、このエラーメッセージが出力されます。

転送元のアカウント内の Report 用 Bucket の中を確認し、 Manifest ファイルの存在と Key が正しいかを確認してください。

Inventory Report が出力されているかを確認します。

2020-11-06/ の Key が存在しない!

原因: Manifest ファイルが存在する Bucket へのアクセス権がない

転送元 Bucket にはクロスアカウントでのアクセスを許可する Bucket Policy の設定が必要です。

Manifest ファイルが存在する Bucket には、 S3 Batch Operations を実行するための IAM Role 以外に 「コンソールで実行するユーザー個人の IAM User または IAM Role」 のアクセス設定が必要になります。

Job 作成時のチェックでは Job 作成を行おうとしている操作者の Role もしくは User が Principal に含まれていることを確認します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowBatchOperationsSourceManfiestRead",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::DestinationAccountNumber:user/ConsoleUserCreatingJob", // ← ここ!
                    "arn:aws:iam::DestinationAccountNumber:role/BatchOperationsDestinationRoleCOPY"
                ]
            },
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion"
            ],
            "Resource": "arn:aws:s3:::ObjectSourceManifestBucket/*"
        }
    ]
}

← ここ! の箇所は、操作するユーザーによって変更します。

Switch Roleをしてる場合は、 :user ではなく :role になりますし、どのユーザーがどのようにコンソールを操作するかによって変更します。自分は、Switch Role で対象環境へアクセスするため以下のような Bucket Policy になります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowBatchOperationsSourceManfiestRead",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::DestinationAccountNumber:role/komuro-hiraku", // ← ここ!
                    "arn:aws:iam::DestinationAccountNumber:role/BatchOperationsDestinationRoleCOPY"
                ]
            },
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion"
            ],
            "Resource": "arn:aws:s3:::ObjectSourceManifestBucket/*"
        }
    ]
}

Inventory Report が出力する Manifest のみ、 Batch Operations の Job 以外にコンソールやCLIを実行するユーザーのアクセス許可ポリシーが必要になります。

原因: Manifest ファイルの所有者が転送元AWSアカウントになっていない

今回一番頭を悩ませた箇所です。

S3 Inventory Report は外部のAWSアカウントにより自動的に Report が作成され、指定された Bucket の中にレポートファイルが作成されます。すると、 デフォルトで何も設定を操作していない場合、以下のようにファイルの所有者が転送元のAWSアカウントではなく、S3 Inventory Report の作成元の AWS アカウントとなります(外部アカウント)

するとこの場合、クロスアカウントアクセスを許可する Bucket Policy をどんなに正しく設定しても、この Manifest ファイルは参照できず、「マニフェストオブジェクトのETag を取得するためのアクセス許可が不十分」というエラーメッセージが転送先の Batch Job の作成段階で出現し続けます。

所有権が原因の場合、対象の Inventory Report が出力ファイルが「転送元 AWS アカウント」を所有者となるように設定する必要があります。

一番簡単な方法は一度 Manifest に関わるデータを全てダウンロードし、アップロードし直す方法です。単純ですが、これで修正可能です。しかし、このままでは毎度 Inventory Report が出力されるたびにアップロードし直す必要があります。根本的に解決しましょう。Inventory Report を出力する Bucket の設定を変更します。

まず Inventory Report を出力する Bucket を選択し、 アクセス許可 タブを選択します。中程にある オブジェクト所有者 をクリックします。

Bucket 作成直後は、選択肢の上段「オブジェクトライター」が選択されているので、これを「希望するバケット所有者」 へ変更します。

この設定によって、以降 Inventory Report で出力されるファイルは、すべて転送元 AWS アカウントが所有者となり、クロスアカウントの設定が有効になります。

注意としては、Inventory Report の出力設定後にこの設定を変更した場合、 Inventory Report 側の Configuration を再度保存し直す必要があります。この手順が抜けると、以下のエラーが出力され Bucket に正常に Inventory Report が出力されません。

再度、設定を開いて保存し直してください。

これで Bucket の設定変更と Inventory Report の出力設定が正常に修正できました。

S3 の所有者権限の委譲についてはこちらに詳しい解説がありますので合わせて御覧ください。

[アップデート] オブジェクト所有権でもう悩まない!S3 バケット所有者がアップロード時に自動的にオブジェクト所有権を引き継げるようになりました。

おまけ: S3 Inventory Report の出力 Bucket に設定する Bucket Policy についての注意

AWS のドキュメントサンプルでは、 Inventory Report が出力する Bucket には以下のような Bucket Policy を設定するよう記載があります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowBatchOperationsSourceManfiestRead",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::DestinationAccountNumber:user/ConsoleUserCreatingJob",
                    "arn:aws:iam::DestinationAccountNumber:role/BatchOperationsDestinationRoleCOPY"
                ]
            },
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion"
            ],
            "Resource": "arn:aws:s3:::ObjectSourceManifestBucket/*"
        }
    ]
}

これをBucket Policy にそのまま貼り付けると Inventory Report の出力で必要な Bucket Policy が消えてしまいます。S3 Inventory Report の出力設定をするとすでに以下の Bucket Policy が設定されているはずです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "InventoryAndAnalyticsExamplePolicy",
            "Effect": "Allow",
            "Principal": {
                "Service": "s3.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::hogehoge-transfer-test/*",
            "Condition": {
                "StringEquals": {
                    "aws:SourceAccount": "XXXXXXXXX",
                    "s3:x-amz-acl": "bucket-owner-full-control"
                },
                "ArnLike": {
                    "aws:SourceArn": "arn:aws:s3:::test-replication-records-20201028"
                }
            }
        }
    ]
}

つまりクロスアカウントアクセスの Statement を 追記する必要があります。 具体例は以下のとおり

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "InventoryAndAnalyticsExamplePolicy",
            "Effect": "Allow",
            "Principal": {
                "Service": "s3.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::hogehoge-transfer-test/*",
            "Condition": {
                "StringEquals": {
                    "aws:SourceAccount": "XXXXXXXXX",
                    "s3:x-amz-acl": "bucket-owner-full-control"
                },
                "ArnLike": {
                    "aws:SourceArn": "arn:aws:s3:::test-replication-records-20201028"
                }
            }
        },
        {
            "Sid": "AllowBatchOperationsSourceManfiestRead",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::DestinationAccountNumber:user/ConsoleUserCreatingJob",
                    "arn:aws:iam::DestinationAccountNumber:role/BatchOperationsDestinationRoleCOPY"
                ]
            },
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion"
            ],
            "Resource": "arn:aws:s3:::ObjectSourceManifestBucket/*"
        }
    ]
}

サンプルを何も考えずにコピペすると、 Inventory Report の出力に必要な Bucket Policy を消してしまい Inventory Report が正常に読み込めなくなるので注意しましょう。 *2

Reading the manifest is forbidden: AccessDenied

Job は作成後にManifest を解析し、実行準備状態へ遷移させます。この Manifest の解析に失敗すると以下のようなエラーが表示されます。

この場合、Job 作成時には manifest.json の存在チェックと読み取れることが確認されています。つまり manifest.json へは正常にアクセスできていますが、 JSON を解析した内容でエラーが発生したことを意味します。

このエラーの原因は次のとおりです。

原因: manifest.json に関連する他のファイルの所有権が転送元の AWS アカウントになっていない

manifest.json の所有権の修正は行ったのですが、 manifest.json で指定されている他のファイル(実際のリストが格納されている gz ファイル)すべての所有権が修正されていなかったため発生しました。 manifest.json そのものの権限チェックは Job 作成時にチェックされますが、JSON の中に書いてあるファイル(リストが格納されている gz ファイル)のチェックは Job 作成後に行われます。

強制的な解決方法としては、一度関連するすべてのファイルをダウンロードして、アップロードし直すと所有権がすべて修正されます。

とはいえ、アップロードされるたびに毎度手作業でダウンロードしてアップロードし直すなどやっていられないので、上記で行った Bucket の所有権の設定を変更することを強くお勧めします。 *3

The job report could not be written to your report bucket.

S3 Batch Operations の Job は完了後に Report を出力することができます。Job 実行で失敗したオブジェクトの情報を Report に記載することができるため、利用することをおすすめします。

Report の出力先として新たに作成した Bucket を指定する場合、エラーが発生する可能性があります。

原因: Batch Operations Job を実行するための Role に Report Bucket の設定がない

これは、Batch Oeprations に割り当てる IAM Role の設定が足りていない可能性があります。AWS のドキュメントのサンプルでは書き込み、読み込みを行う Bucket には以下の 3 つが指定されています。

  • データ転送元の Source Bucket: ObjectSourceBucket
  • データ転送先の Destination Bucket: ObjectDestinationBucket
  • Inventory Report の Manifest を管理しているデータ転送元の Bucket: ObjectSourceManifestBucket

よく見るとたしかに Report 出力の Bucket 設定はないようです。コピー先の Bucket と同じものを利用すれば問題ないようですが、コンテンツと Report が混じるのはあまりよろしくなさそうなので、私は明確に分離しました。

{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "AllowBatchOperationsDestinationObjectCOPY",
                "Effect": "Allow",
                "Action": [
                    "s3:PutObject",
                    "s3:PutObjectVersionAcl",
                    "s3:PutObjectAcl",
                    "s3:PutObjectVersionTagging",
                    "s3:PutObjectTagging",
                    "s3:GetObject",
                    "s3:GetObjectVersion",
                    "s3:GetObjectAcl",
                    "s3:GetObjectTagging",
                    "s3:GetObjectVersionAcl",
                    "s3:GetObjectVersionTagging"
                ],
                "Resource": [
                    "arn:aws:s3:::ObjectDestinationBucket/*",
                    "arn:aws:s3:::ObjectSourceBucket/*",
                    "arn:aws:s3:::ObjectSourceManifestBucket/*"
                ]
            }
        ]
}

Report 出力向けの Bucket を別に設定したい場合は、 Batch Operations の Role に当該 Bucket も含める必要があります。Resource に追加して修正。サンプルとして Report 出力する Bucket は CompleteReportBucket という名前とします。

                "Resource": [
                    "arn:aws:s3:::ObjectDestinationBucket/*",
                    "arn:aws:s3:::ObjectSourceBucket/*",
                    "arn:aws:s3:::ObjectSourceManifestBucket/*",
                    "arn:aws:s3:::CompleteReportBucket/*" // ← 追加
                ]

Role修正後、 Job をクローンして再作成するとこちらのエラーは解消します。

まとめ

クロスアカウントアクセスにおけるエラー周りの調査は、なかなかエラーメッセージから読み取ることが難しく、全体像を把握した上で論理的に不明瞭な箇所(特に Policy 周り)がないかを確認していきました。特にクロスアカウントアクセスの場合、404 に相当するエラーが 403 Forbidden で返されるなど、前提とした知識がないと明後日の方向を調査してしまい、原因究明までに非常に時間がかかるものでした。

オブジェクトの所有者の問題については、正直 [アップデート] オブジェクト所有権でもう悩まない!S3 バケット所有者がアップロード時に自動的にオブジェクト所有権を引き継げるようになりました。 このブログを読んで事前に頭に入っていなかったら解決できなかったと思います。

S3 の Bucket Policy ははじめて真面目に向き合えました。現場で実際に使ってみるとドキュメント通りそのまま適用できるのは稀で、自身の理解不足を感じます。IAM の Role や Policy, Bucket Policy や Block Public Access 設定などなど未だにきちんとすべてを理解できたわけではないのですが、実際の案件でしっかり使うことで少しだけ理解できた気がします。

今後もエラーに対して、なし崩し的にとりあえず対応したり、設定をコピペして終わりにするのではなく、原因を理解し考えて対応をするようにしていきたい。

参照

脚注

  1. データの一部が欠損した状態になるため
  2. 自分はやらかして、一日無駄にしました。
  3. ワンショット作業なら別に手でやっても大した作業ではないので、そのままでも良いかも