バックアップからリストアしたDynamoDB テーブルに格納されているデータをAWS Glueを用いて元のテーブルに複製する
こんにちは、なおにしです。
標題の内容について検証する機会がありましたのでご紹介します。
はじめに
DynamoDB テーブルはポイントインタイムリカバリ (PITR) またはAWS Backup を使用することでバックアップが可能です。
また、AWS Backup を使用せずにDynamoDBの機能としてオンデマンドバックアップを取得することも可能ですが、いずれの場合でも共通しているのは、リストアに際してバックアップ元のテーブルを上書きするのではなく、新しいテーブルが作成されるということです。
リストアされた新しいテーブルをそのまま使用するのであれば問題ありませんが、特にCloudFormationなどのIaCを用いてDynamoDB テーブルを作成している場合、管理対象のリソースから外れてしまうという課題があります。
スタックを更新してインポートしたり、そもそもDynamoDB テーブルを別管理にしたりなど対応方法は色々考えられますが、例えばAmplify を使用している場合などはさらに複雑になってくるかと思います。
このため、リストアして作成された新しいDynamoDB テーブルから効率的にデータをコピーすることができれば、IaCで管理されているDynamoDB テーブルに対しても結果的にそのままリストアできますし、IaCで作成したステージング環境のDynamoDBテーブルに本番環境のバックアップからデータを投入するということもできるようになります。
このような操作を実現するためのサービスとして思いつくのがAWS Data Pipeline ではないかと思うのですが、Data Pipelineは2024年7月末から新規顧客の利用ができなくなっています。
上記ドキュメントに記載されているとおり、Data Pipelineからのワークロードの移行先としてはGlueやStep Functionsなどが挙げられています。
というわけで、Glueを使用してDynamoDB テーブルから別のDynamoDB テーブルに対するデータのコピーを試してみます。
なお、一括りにGlueといっても多くの機能があるため、正確にはSpark Jobs 機能を使用してETLを実行します。Glueについては以下の記事もご参照ください。
やってみた
DynamoDB テーブルの準備
バックアップ対象のDynamoDB テーブルには2レコードが登録済みとしました。また、対象のテーブルは図のとおりの属性で複合プライマリキーを設定しています。
上記に対して取得したバックアップからリストアしたDynamoDB テーブルは以下のとおりです。当然ですがデータの内容は同じになっています。
リストアの結果がどうなるかを確認するために、バックアップ元(= リストア先)のDynamoDB テーブルに登録されているレコードを以下のように変更しました。バックアップ時には存在していた2レコードを削除し、新規レコードを追加しています。ただし、1つはあえて削除前に存在していたレコードと同じ複合プライマリキーを指定しており、その他の属性(コメントなど)だけ異なる状態にしています。
これは、以下2点を確認したい意図です。
-
バックアップに存在するプライマリキーを持ったレコードが上書きされるかどうか
-
バックアップには存在しないプライマリキーを持ったレコードがどうなるか
Glue用IAMロールの作成
Getting started からフォームに沿って作成することができます。
ただし、今回はドキュメントに従って「AWSGlueServiceRole」というIAMロールを作成し、以下のマネージドポリシーをアタッチしました。ドキュメントには「AWSGlueConsole-S3-read-only-policy」ポリシーや「AWSGlueConsole-S3-read-and-write」ポリシーの記載がありますが、「AWSGlueConsoleFullAccess」ポリシー内で「aws-glue-」から始まる名称のS3バケットについてはアクセス権が設定されているのでこちらは割愛しています。
- AWSGlueServiceRole(若干紛らわしい感がありますが管理ポリシー名です。)
- AWSGlueConsoleFullAccess
- AmazonDynamoDBFullAccess_v2
3つ目のDynamoDBに関する管理ポリシーについては、こちらのドキュメントに以下の記載があるため追加しています。必要なポリシーが具体的に記載されていなかったため一旦はFullAccess権限を付与していますが、本番運用する場合は必要な権限を精査して適用することを推奨します。
AWS Glue から DynamoDB に接続するには、AWS Glue ジョブに関連付けられた IAM ロールに DynamoDB と対話する権限を付与します。
ちなみに、DynamoDBに対するアクセスを設定していなかった場合、以下のエラーが出力されてジョブが失敗しました。
Error Category: UNCLASSIFIED_ERROR; Failed Line Number: 26; An error occurred while calling o255.pyWriteDynamicFrame. User: arn:aws:sts::012345678912:assumed-role/AWSGlueServiceRole/GlueJobRunnerSession is not authorized to perform: dynamodb:DescribeTable on resource: arn:aws:dynamodb:ap-northeast-1:012345678912:table/Attendance-zzao6o2dhrfqrboowp5vxtyy3q-NONE because no identity-based policy allows the dynamodb:DescribeTable action (Service: DynamoDb, Status Code: 400, Request ID: K05HSGMN6NDFS841TMMP8EUN0VVV4KQNSO5AEMVJF66Q9ASUAAJG)
余談:管理ポリシー「AmazonDynamoDBFullAccess_v2」が新しくできたようです
完全に余談ですが、「AmazonDynamoDBFullAccess_v2」、本記事の執筆時点で以下のとおりとても新しいポリシーですね。
- Creation time: May 22, 2025, 14:52 UTC
- Edited time: May 22, 2025, 14:52 UTC
「AmazonDynamoDBFullAccess」と比較すると、Data Pipeline関係の権限がごっそり削除されていたり、SNSやLambda関係の権限も無くなっているようです。ドキュメントには以下の記載があり、「AmazonDynamoDBFullAccess」は廃止(Deprecated)になったようなのでご注意ください。
This policy has been replaced by a scoped-down policy named
AmazonDynamoDBFullAccess_v2
.After April, 2025, you can't attach the
AmazonDynamoDBFullAccess
policy to any new users, groups, or roles.
別記事としてまとめようかとも思いましたが、AWS 管理ポリシーが廃止(非推奨化)され新しいポリシーが作成される、というのは稀によくある話ということなので余談に留めます。
Glue ETLジョブの作成
以下のとおり[ETL jobs]-[Script editor]を選択します。
デフォルトで以下のとおりになっているかと思いますので、そのまま[Create script]を選択します。
以下のドキュメントを参考に、DynamoDB テーブル内のデータをコピーするスクリプトを準備します。
import sys
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job
from awsglue.utils import getResolvedOptions
args = getResolvedOptions(sys.argv, [
'JOB_NAME',
'source_table',
'target_table',
'dynamodb_splits'
])
glue_context= GlueContext(SparkContext.getOrCreate())
spark = glue_context.spark_session
job = Job(glue_context)
job.init(args["JOB_NAME"], args)
dyf = glue_context.create_dynamic_frame.from_options(
connection_type="dynamodb",
connection_options={"dynamodb.input.tableName": args['source_table'],
"dynamodb.splits": args['dynamodb_splits']
}
)
glue_context.write_dynamic_frame_from_options(
frame=dyf,
connection_type="dynamodb",
connection_options={"dynamodb.output.tableName": args['target_table'],
}
)
job.commit()
上記を[Script]タブに記載します。また、スクリプト名も画面左上から設定できます。
[Job details]タブを選択して編集します。[IAM Role]の設定は必須なので、先ほど作成したロールを選択します。
画面を下にスクロールし、[Requested number of workers]だけ「2」に変更します(デフォルト:10)。今回はDynamoDBテーブルのサイズも非常に小さいことから、設定可能な最小値を指定しています。
こちらはデータ処理ユニット (DPU) に関わっており、[Worker type]で「G 1X」選択しているため、設定可能な最小値が「2」になっています。
Glueの料金ページには以下の記載があります。
AWS Glue のジョブには、Apache Spark、Apache Spark Streaming、Ray (プレビュー)、Python シェルの 4 つのタイプがあります。Spark および Spark ストリーミングジョブの実行には、最低 2 DPU が必要です。デフォルトで、AWS Glue は各 Spark ジョブに 10 DPU を、各 Spark Streaming ジョブには 2 DPU を割り当てます。AWS Glue バージョン 2.0 以降を使用するジョブについては、1 分間分の最低料金がかかります。
さらに、ドキュメントには以下の記載があります。
G.1X ワーカータイプでは、各ワーカーは 1 DPU (4 vCPU、16 GB のメモリ、94 GB のディスク) にマッピングされており、ワーカーごとに 1 個のエグゼキュターを提供します。データ変換、結合、クエリなどのワークロードには、ほとんどのジョブを実行するためのスケーラブルで費用対効果の高い方法として、このワーカータイプをお勧めします。
インスタンスタイプやDPUに関しては以下の記事もご参照ください。
続いて、[Advanced properties]については以下の部分はデフォルトのままです。なお、バケット名が自動で入力された状態になっているかと思いますが、このタイミングでは実際に作成していなくても問題ありません。
さらに画面下部にスクロールし、[Job parameters]にはスクリプトに記載されている[JOB_NAME]以外の引数を定義します。
dynamodb.splits
については、ドキュメントに以下の記述があったため今回は「4」を設定しています。numSlots
の計算方法についてもドキュメントをご参照ください。
dynamodb.splits
を使用可能なスロット数、numSlots
に設定することをおすすめします。
ドキュメントに記載されている元々のスクリプトでは"dynamodb.splits": "100"
と定義されていましたが、このまま実行するとソースのDynamoDB テーブルに対して100回のScanが実行されるようです。以下は"dynamodb.splits": "50"
にしてみた場合のメトリクスです。
ここまで設定が完了したら画面右上の[Save]を押下します。
なお、[Save]を押下したタイミングで先ほどの[Advanced properties]に記載されていたS3バケットが自動的に作成されるので、当該バケットを事前に作成しておく必要はありません。
Glue ETLジョブの実行
ジョブを[Save]すると[Run]ボタンが選択できるようになるので押下します。[Runs]タブに移動すると、以下のようにジョブが実行されていることが分かります。
成功すると以下のようになります。(Failedのジョブが存在するのは、ここでDynamoDBに対する権限の追加が必要なことに気付いたためです。。)
リストア結果の確認
リストアされたDynamoDB テーブル内のデータは以下のとおりです。
無事にデータが書き込まれています。また、矢印の部分に着目すると分かるように、確認したかった観点の結果は以下のとおりです。
-
バックアップに存在するプライマリキーを持ったレコードが上書きされるかどうか
- 上書きされる
-
バックアップには存在しないプライマリキーを持ったレコードがどうなるか
- Glueによってデータが書き込まれた後も残っている(上書きされない)
以上より、同じプライマリキーを持つ更新されたデータが存在する場合は少し注意が必要ですが、逆にいうとその場合でもジョブが失敗するわけではないといえます。
補足:1MBを超えるDynamoDB テーブルでもコピー可能です
DynamoDBでは、1 回のScanリクエストで最大1MBのデータが取得できるという制約があります。
こちらの記事にあるとおり、1MB以上のデータを取得する場合はLastEvaluatedKey
を使用して全件取得できるまでループ処理を実装する必要があります。
このため、もし本記事で取り組んだ内容をGlueを使用せずにスクリプトのみで実装する場合は同様の考慮が必要となります。
今回のGlueを使用したパターンでは、使用したスクリプトはPySpark 拡張機能を用いてDynamoDB テーブルにアクセスするものだったため、前述のような考慮を明示的に実装しているわけではありません。
そこで、実際に1MBを超えるテーブルを準備して再度データをコピーしてみます。
データソースとなるDynamoDB テーブルには、以下のように追加でレコードを格納しました。
格納されているレコード数は以下のとおりです。
CLIを使用してScanを実行してみます。
$ aws dynamodb scan --table-name Attendance-zzao6o2dhrfqrboowp5vxtyy3q-NONE-restored --no-paginate | tail -20
},
"mailAddress": {
"S": "3086c72e-a58b-4945-b8c9-b5d7b16c7cec@test.example.com"
},
"timeSlot": {
"S": "夜"
}
}
],
"Count": 1180,
"ScannedCount": 1180,
"LastEvaluatedKey": {
"dateTimeSlot": {
"S": "2025-05-15-夜"
},
"mailAddress": {
"S": "3086c72e-a58b-4945-b8c9-b5d7b16c7cec@test.example.com"
}
}
}
レコード数「4002」に対して取得できた数は「1180」であり、LastEvaluatedKey
が返ってきていますので一度のScanで取得可能なレコード数を超過したデータがテーブルに格納されていることが分かります。また、テストデータの1レコードあたりのサイズはほぼ同じであるため、割合から考えるとテーブルに格納されている総容量は4MB弱です。
なお、上記で実行しているAWS CLIについて補足ですが、--no-paginate
オプションをつけずに実行すると複数回APIを発行してデータを結合して表示してくれるので、1MB制限を意識する必要がない挙動がデフォルトになっています。この辺りについてはドキュメントをご参照ください。
改めてGlueで同じジョブを実行したところ、以下のとおり問題なく完了しました。
選択されている一番上のジョブが4MB弱のデータを格納した状態のもので、それ以外のジョブは2レコードだけを格納した状態で何度か試行した結果なのですが、Duration
のとおり今回の検証レベルのデータ量であれば実行時間に差が出ないようです。
もちろんコピー先のテーブルでデータも確認できています。
前述の検証の流れで4000件のテストデータを追加している状態なので、コピー先のテーブルではコピー元のテーブルのレコード数+1になります。
補足:CLIでジョブを実行する場合
以下のコマンドでジョブを実行することができます。[Job details]-[Advanced properties]-[Job parameters]で指定されている引数よりもCLIで指定した引数の方が優先されます。
aws glue start-job-run \
--job-name DynamoDB_Table_Restore \
--arguments '{
"--source_table":"Attendance-zzao6o2dhrfqrboowp5vxtyy3q-NONE-restored",
"--target_table":"Attendance-zzao6o2dhrfqrboowp5vxtyy3q-NONE",
"--dynamodb_splits":"4"
}'
まとめ
元々はGlue StudioのVisual ETLを用いてノーコードでサクッと実現可能なのでは?と期待して検証を始めたのですが、データソースとしてDynamoDB テーブルを設定した後、同様にターゲットにもDynamoDBテーブルを指定しようとしたところで、ターゲットにはDynamoDBテーブルを指定できないことに気付いてスクリプト処理に方向転換しました...(ソースにDynamoDBを指定する際にはPITRを有効化した方が良いとか色々ハマった後に気付いたのでダメージ大きめでした。先に確認しとけば良かったです)
とはいえ、ドキュメントに記載されていたサンプルコードをほとんどそのまま使うことができましたし、非常に短い記述で実装できたのでとても便利だと感じました。
今回のようにリストアによって作成した新規テーブルをソースとしてターゲットのテーブルにデータを書き込むのであれば、ソーステーブルに対してGlueが実行する読み取り処理についてはそれほど意識する必要はないかと思いますが、ターゲットのテーブルが利用中なのであればスクリプトでスループットの調整をご検討ください。
また、格納されているデータ量によってはジョブの実行時間が長くなる可能性もあるので、その際はワーカー数やsplits
の調整も必要になってくるかと思いますが、逆にいうとETLツールとしてこのように流量を調整できることも非常に便利だと思います。
本記事がどなたかのお役に立てれば幸いです。