AWS Labsの「Bulk Executor for Amazon DynamoDB」でDynamoDBの一括操作をAWS Glueに任せてみる
はじめに
データ事業本部のkobayashiです。
DynamoDBのテーブルに対して「全件カウントしたい」「特定条件のアイテムをまとめて削除したい」「別テーブルに差分をコピーしたい」「テスト用にサンプルデータを1億件流し込みたい」といった一括処理を行いたい場面は、開発や運用の中でよく出てきます。ただ、テーブルが大きくなるとboto3で愚直にスキャンや書き込みをしていては現実的な時間で終わらず、AWS Glueで並列ジョブを組むにしても、コードやコネクタ設定を都度書くのはそれなりに手間がかかります。
そのような場面で役立つツールとしてAWS Labsから公開されている Bulk Executor for Amazon DynamoDB があります。CLIのように振る舞いながら、裏側でAWS Glueの並列Sparkジョブを起動して大規模テーブルに対する一括操作を実行してくれるツールです。
今回はこのBulk Executorを実際に手元でセットアップし、100万件のダミーデータを流し込んでscancount・count --where・find --orderbyを回し、その実行ログとGlueジョブ実測(DPU時間・所要時間)まで含めてまとめます。
Bulk Executor for Amazon DynamoDBとは
Bulk ExecutorはAWS Labsが公開しているDynamoDB向けの一括操作ツールで、コマンドラインから./bulk <verb> --table <table>のような形で呼び出すと、裏側でAWS Glue上のSparkジョブが起動し、DynamoDBに対して並列処理を行ってくれます。
主な特徴としては以下になります。
- CLIライクな操作感:
./bulk count,./bulk find,./bulk deleteのように動詞ベースでコマンドを実行 - AWS Glueによる並列実行: 内部でGlueのSparkジョブを呼び出し、大規模テーブルに対しても並列度の高い処理を実現
- 豊富な組み込みアクション: カウント、検索、削除、更新、フィル、比較、コピー、SQL、エクスポートロード等が最初から使える
- Spark SQL構文でのフィルタ:
--whereにSpark SQLの構文で条件を指定できるため、payload > 'a' and ts < '2024'のような複合条件も表現可能 - スロットリング管理:
--XMaxReadRate/--XMaxWriteRateで1秒あたりの読み書きユニットの上限を指定できる - コスト見積もりの表示: 各コマンドの実行前にDynamoDBのコスト見積もりが表示され、Ctrl-Cで中断可能
- PITR必須の安全装置: テーブルを書き換えるコマンドはPITR(Point-in-Time Recovery)が有効なテーブルにしか適用できない
- 拡張性:
client/src/python_modules/(クライアント側)とserver/src/python_modules/(サーバー側)にPythonスクリプトを配置してbootstrapし直すだけで独自の動詞を追加可能
利用可能な組み込みアクション
./bulkをverbなしで実行すると、以下のアクションが利用可能なことがわかります。
| アクション | 概要 |
|---|---|
bootstrap |
Glueジョブ、IAMロール、S3バケット等のバックエンドリソースを作成 |
teardown |
bootstrapで作成したリソースを削除 |
count |
DynamoDBコネクタ経由でアイテムをカウント(--whereでSpark SQL条件指定可能) |
find |
条件に一致したアイテムを出力(10件までコンソール、それ以上はS3) |
delete |
条件に一致したアイテムを削除 |
fill |
Pythonジェネレータスクリプトで生成したフェイクデータを大量投入 |
update |
Pythonジェネレータスクリプトが返す更新式で並列アップデート |
scancount |
DynamoDBのSelect=COUNTを使ってメモリ効率よくカウント(連携GSI対応) |
diff |
2つのテーブルを比較して差分を出力 |
sql |
任意のSpark SQLをテーブルに対して実行 |
copy |
テーブル間コピー(クロスアカウント/クロスリージョン対応) |
load |
S3上のCSV/JSON/ParquetファイルからDynamoDBへロード |
load-export |
DynamoDBのS3エクスポートを別テーブルへロード |
revert-export |
差分エクスポートの内容を打ち消す方向で書き戻し |
Bulk Executorを試してみる
環境
今回使用した環境は以下の通りです。
Python 3.11.10
boto3 1.43.40
Bulk Executor v1 (main HEAD, 2026-07時点)
リージョン: ap-northeast-1
前提条件
Bulk Executorのbootstrapを行うには以下が必要です。
- Python 3.10以上
boto3- IAM、Glue、S3、CloudWatch Logsに対する管理者相当の権限(詳細はREADMEの
bootstrap用ポリシー参照) - 実行環境で CloudWatch Logs用のVPCエンドポイントが設定されていないこと
- Bulk ExecutorはCloudWatch LiveTail APIに依存するため、VPCエンドポイント配下だとLiveTailが動きません
- CloudShellはこの理由で基本的に非対応です(別リージョンをターゲットにすれば可)
リポジトリのクローンとインストール
amazon-dynamodb-toolsリポジトリをcloneし、tools/bulk_executorディレクトリでmake installを実行するとvenvが作成され、boto3とテスト用の依存が入ります。
$ git clone https://github.com/awslabs/amazon-dynamodb-tools.git
$ cd amazon-dynamodb-tools/tools/bulk_executor
$ make install
インストール後、./bulkをverbなしで実行するとヘルプと利用可能なアクション一覧が表示されます。
$ ./bulk
Welcome to the Bulk Executor for Amazon DynamoDB!
This tool is useful for quickly executing bulk commands even large tables.
It uses AWS Glue internally to perform parallel execution.
Full documentation can be found at: https://github.com/awslabs/amazon-dynamodb-tools/blob/main/bulk_executor/README.md
Available actions found in client/src/python_modules:
- bootstrap
- copy
- count
- delete
- diff
- fill
- find
- load
- load-export
- revert-export
- scancount
- sql
- teardown
- update
bootstrapで環境を作成する
Bulk Executorは実行の下地としてGlueジョブ、IAMロール、S3バケット、CloudWatchロググループを一気に作成するbootstrapアクションを持っています。今回は書き換え系verb(fill)まで試すので、READ-WRITEロールで初期化します。
$ ./bulk bootstrap --XRole READ-WRITE
Running action 'bootstrap' with arguments: {'verb': 'bootstrap', 'XRole': 'READ-WRITE'}
Adding Glue Job role...
READ-WRITE role pre-configured.
Bulk Executor Glue Job Role created: AWSGlueServiceRoleBulkDynamoDB-DdbReadWrite-ap-northeast-1
Creating CloudWatch log groups for Glue job...
Log group '/aws-glue/jobs/error' already exists.
Updated retention policy for existing log group /aws-glue/jobs/error
Log group '/aws-glue/jobs/output' already exists.
Updated retention policy for existing log group /aws-glue/jobs/output
Created Glue connection 'bulk-dynamodb-connection' for DynamoDB DataFrame source.
Attaching Glue Job Role arn:aws:iam::123456789012:role/AWSGlueServiceRoleBulkDynamoDB-DdbReadWrite-ap-northeast-1 to Bulk Executor Glue Job...
Bulk Executor Glue Job created successfully.
Bucket 'aws-glue-bulk-dynamodb-ap-northeast-1-123456789012-bn2aieu6r' created successfully!
Glue script 'server/src/root.py' uploaded into S3 successfully.
Successfully zipped .../server/src/python_modules to .../client/src/infrastructure/tmp/python_modules.zip using Python zipfile
Python modules archive 'client/src/infrastructure/tmp/python_modules.zip' uploaded into S3 successfully to 'server/src/python_modules.zip'.
Properties files 'server/src/log4j2.properties' uploaded into S3 successfully!
Bulk Executor environment configurations:
AWS -
Account: 123456789012
Region: ap-northeast-1
Role: arn:aws:sts::123456789012:assumed-role/xxxxxxx/botocore-session-xxxxxxxxxx
数十秒ほどで完了し、以下のリソースが自動で作られていることが確認できます。
- IAMロール:
AWSGlueServiceRoleBulkDynamoDB-DdbReadWrite-ap-northeast-1 - CloudWatchロググループ:
/aws-glue/jobs/error,/aws-glue/jobs/output(保存期間365日) - Glue接続:
bulk-dynamodb-connection - Glueジョブ:
bulk_dynamodb - S3バケット:
aws-glue-bulk-dynamodb-ap-northeast-1-123456789012-bn2aieu6r(ランダムサフィックス付き)
--XRoleに指定する値によって、作られるIAMロールの権限がわかりやすく切り替わります。
| 指定値 | Glueジョブに付与されるDynamoDB権限 |
|---|---|
READ-ONLY |
AmazonDynamoDBReadOnlyAccess(読み取りのみ) |
READ-WRITE |
AmazonDynamoDBFullAccess(読み書き。テーブル書き換え系のverbを使う場合はこちら) |
| 既存ロール名 | 自分で作ったIAMロールをそのまま利用(teardownでは削除されない) |
なお、既存ロールを指定する場合は以下の要件を満たす必要があります(詳細はREADME参照)。
- ロール名が
AWSGlueServiceRoleで始まること - Glueサービスプリンシパル(
glue.amazonaws.com)を許可する信頼ポリシーがあること - 管理ポリシー
AWSGlueServiceRoleがアタッチされていること - サービスクォータ読み取り権限(
ServiceQuotasReadOnlyAccessまたはservicequotas:GetServiceQuota/GetAWSDefaultServiceQuota) - 料金情報読み取り権限(
AWSPriceListServiceFullAccessまたはpricing:GetProducts) - 対象テーブルに応じたDynamoDBアクセス権限(
AmazonDynamoDBReadOnlyAccess/AmazonDynamoDBFullAccess、もしくは特定テーブルに絞ったカスタムポリシー)
作成されたリソースをAWS CLIで確認する
Glueジョブが実際に登録されているか、aws glue get-jobで確認します。
$ aws glue get-job --job-name bulk_dynamodb --region ap-northeast-1 \
--query 'Job.[Name,Role,GlueVersion,MaxRetries,Timeout,NumberOfWorkers,WorkerType,ExecutionClass]' \
--output table
----------------------------------------------------------------------------------------------
| GetJob |
+--------------------------------------------------------------------------------------------+
| bulk_dynamodb |
| arn:aws:iam::123456789012:role/AWSGlueServiceRoleBulkDynamoDB-DdbReadWrite-ap-northeast-1 |
| 5.1 |
| 0 |
| 60 |
| 220 |
| G.1X |
| None |
+--------------------------------------------------------------------------------------------+
GlueVersion=5.1のジョブが、NumberOfWorkers=220、WorkerType=G.1X、Timeout=60(分)、MaxRetries=0の設定で作られていることがわかります。これらのデフォルト値は各アクションの--XNumberOfWorkers, --XWorkerType, --XTimeout等で個別に上書き可能です。ExecutionClassがNone表示になっていますが、これはジョブ定義に固定されておらず、StartJobRun側で毎回指定される仕組みのためです。未指定なら実行時にSTANDARD扱いになります。
S3バケットとIAMロールも合わせて確認します。
$ aws s3 ls | grep aws-glue-bulk-dynamodb
2026-07-17 16:56:51 aws-glue-bulk-dynamodb-ap-northeast-1-123456789012-bn2aieu6r
$ aws iam list-roles \
--query 'Roles[?starts_with(RoleName,`AWSGlueServiceRoleBulkDynamoDB`)].RoleName' \
--output text
AWSGlueServiceRoleBulkDynamoDB-DdbReadWrite-ap-northeast-1
アクションごとのヘルプを確認する
各verbの詳細な引数は./bulk <verb> --helpで確認できます。例えばscancountのヘルプは以下のようになります。
$ ./bulk scancount --help
usage: bulk scancount [-h] [--XExecutionClass {STANDARD,FLEX}]
[--XTimeout XTIMEOUT]
[--XNumberOfWorkers XNUMBEROFWORKERS]
[--XWorkerType {G.1X,G.2X,G.4X,G.8X,G.12X,G.16X,R.1X,R.2X,R.4X,R.8X}]
[--XWaitForDPU] [--XContinuousLogging]
[--XMaxWriteRate XMAXWRITERATE]
[--XMaxReadRate XMAXREADRATE] [--XAccount XACCOUNT]
[--XRegion XREGION] --table TABLE
[--filter-expression FILTER_EXPRESSION]
[--expression-names EXPRESSION_NAMES]
[--expression-values EXPRESSION_VALUES] [--index INDEX]
Purpose of "scancount":
Count items in a DynamoDB table, using a scan instead of a DynamoDB Connector (so usually faster)
Required --table parameter
Optional --index parameter if scanning a secondary index
Optional --filter-expression parameter to specify a push-down FilterExpression predicate
Optional --expression-names parameter to specify the expression names used in the filter-expression
Optional --expression-values parameter to specify the expression values used in the filter-expression
Examples:
# Count all items in a table
bulk scancount --table orders
# Count using a filter expression (uses DynamoDB FilterExpression syntax)
bulk scancount --table audit --filter-expression "#touched > :touched" --expression-names '{"#touched": "touched"}' --expression-values '{":touched":1742359403.0}'
--tableが必須で、--filter-expressionにはDynamoDB本来のFilterExpression構文が使えます。フィルタ済みのアイテムだけをSpark側に返す形になるため、メモリ効率やSparkへの転送量の面で有利になる場合があります。ただしDynamoDB側の課金対象はフィルタ前の読み込み量である点は通常のScan同様です。
実機で試してみる:100万件のダミーデータでfill → scancount → count → find
ここからは実際にDynamoDBテーブルを1つ用意し、100万件のダミーデータを流し込んでから読み取り系のverbを実測してみます。今回使ったverbはいずれも1回の実行で1回ずつGlueジョブが起動する構造なので、--XWaitForDPUを付けて実行終了時に消費DPU時間を表示するようにしました。
事前準備:デモ用テーブル作成とPITR有効化
まず、書き換え系verbが動くPITR有効な bulk-demo-202607 テーブルをOn-Demandモードで作成します。PITRはテーブル作成直後だとエラーになる場合があるので少し待ってから有効化しています。
$ aws dynamodb create-table \
--table-name bulk-demo-202607 \
--attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \
--key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST \
--region ap-northeast-1
$ aws dynamodb wait table-exists --table-name bulk-demo-202607 --region ap-northeast-1
$ aws dynamodb update-continuous-backups \
--table-name bulk-demo-202607 \
--point-in-time-recovery-specification PointInTimeRecoveryEnabled=true \
--region ap-northeast-1
{
"ContinuousBackupsDescription": {
"ContinuousBackupsStatus": "ENABLED",
"PointInTimeRecoveryDescription": {
"PointInTimeRecoveryStatus": "ENABLED",
"RecoveryPeriodInDays": 35,
"EarliestRestorableDateTime": "2026-07-17T16:59:48+09:00",
"LatestRestorableDateTime": "2026-07-17T16:59:48+09:00"
}
}
}
書き換え系のverb(fill, update, delete, copy, load, load-export, revert-exportなど、READMEで「mutation」として扱われるもの全般)は、PITRが有効なテーブルにしか適用できない安全装置になっています。copyはsource/target両方がpitr_enabled=Trueで検証される実装(copy.py)になっており、同一アカウント内であれば両方でPITRを有効にしておくのが基本です。クロスアカウントでPITR状態を取得できない場合は警告を出したうえでスキップする分岐がありますが、想定外の停止を避けたい場合はコピー元/コピー先ともPITRを有効化しておくのが安全です。
100万件のダミーデータをfillで投入
fillはPythonのgenerate()関数を持つジェネレータスクリプトをserver/src/python_modules/fill/配下に配置してからbootstrapしておくと、そのジェネレータを並列で呼び出しながらアイテムを大量生成できます。今回はリポジトリ同梱のdefaultジェネレータ(pk/sk/#meta or payloadを持つ3種類のアイテムを1回のgenerate()呼び出しで返す)で100万件流し込みます。README推奨のFakerライブラリはfill実行時にGlueの追加Pythonモジュールとして自動的に指定されるため、自作ジェネレータでもfrom faker import Fakerで使えます。
$ ./bulk fill --table bulk-demo-202607 --generator default --numitems 1000000 --XWaitForDPU
Running action 'fill' with arguments: {'verb': 'fill', 'table': 'bulk-demo-202607', 'numitems': 1000000, 'generator': 'default', 'XWaitForDPU': True}
The bulk executor job cost consists of DynamoDB and Glue costs
For small jobs, the Glue cost portion is usually dominating
Using fewer Glue workers for small jobs, through the --XNumberOfWorkers parameter, will often reduce the Glue costs
For large jobs, where the cost is more significant, the DynamoDB cost portion is usually dominating
...
Starting Bulk Executor Glue Job...
Bulk Executor Glue Job started with job run ID: jr_cc8cd8a2f914848a358300957e2b509deb308f9111e1315a398af51205ef8f43
Job start time: 2026-07-17 17:00:14.777565
Press Ctrl+C to cancel Bulk Executor Glue Job.
Waiting 40 seconds for DPU metrics to gather...
Job completed successfully. Job duration: 0:05:48 (2.00 DPU hours)
Table: 'bulk-demo-202607'
Billing mode: On-demand
Table Item Count (approx): 0
Table Size (approx): 0 bytes
DynamoDB fill costs depend on how many items are being written and the size of the items.
Here we assume the command will insert 1,000,000 items
with average size 161 bytes (based on peeking at generator output);
each write incurs an average of 1 write units
Write units required to do such a fill (approx): 1,000,000
This does not include costs for secondary indexes!
Approx DynamoDB cost for On-demand writes consuming 1,000,000 WRUs (using ap-northeast-1 prices): $0.72
[bulk-demo-202607] Max write rate set to account quota limit: 40000
Total records filled: 1,000,000
100万件の投入が 5分48秒 で完了し、Glueは 2.00 DPU時間 を消費、DynamoDBのOn-Demand書き込みコスト見積もりは $0.72 と表示されました。ジョブ実行前にはコスト内訳が説明され、コストが高いと感じたらCtrl-Cで中断できるようになっている点は運用ミス予防としてありがたい仕組みです。なおTable Item Count (approx): 0と表示されていますが、これはDescribeTableの内部統計が約4時間ごとにしか更新されない仕様のためで、実際は100万件書き込まれた直後の状態です。
scancountで全件カウント
scancountはDynamoDB側でSelect=COUNTを使うため、DynamoDBコネクタでアイテムをSpark側に読み込むcountより軽量です。実測してみます。
$ ./bulk scancount --table bulk-demo-202607 --XWaitForDPU
Running action 'scancount' with arguments: {'verb': 'scancount', 'table': 'bulk-demo-202607', 'XWaitForDPU': True}
...
Starting Bulk Executor Glue Job...
Bulk Executor Glue Job started with job run ID: jr_1b026b88f39db20907cdef029f3625b7c13636772f2d391fc37ebbfe08c55aa8
Job start time: 2026-07-17 17:23:42.726485
Press Ctrl+C to cancel Bulk Executor Glue Job.
Waiting 40 seconds for DPU metrics to gather...
Job completed successfully. Job duration: 0:02:07 (0.87 DPU hours)
Table: 'bulk-demo-202607'
Billing mode: On-demand
...
[bulk-demo-202607] Max read rate set to account quota limit: 40000
Total records counted: 1,000,000
100万件のカウントが 2分7秒、0.87 DPU時間 で完了しました。fillとの差(2.00 vs 0.87 DPU時間)は、書き込み側の方がスケールアウト・ネットワーク帯域とも重かったことを反映していそうです。
scancountは他にも以下のような使い分けができます。
以下はbulk-demo-202607ではなく、GSIやtouched属性を持つ別テーブル(例: orders)を想定した使い分けの例です。
# スパースGSIをスキャンして特定条件を持つアイテム数だけカウント
$ ./bulk scancount --table orders --index i
# DynamoDBのFilterExpression構文で述語プッシュダウン
$ ./bulk scancount --table orders \
--filter-expression "#touched > :touched" \
--expression-names '{"#touched": "touched"}' \
--expression-values '{":touched":1742359403.0}'
--filter-expressionはDynamoDB本来のFilterExpression構文でDynamoDB側にフィルタが渡るため、フィルタ済みのアイテムだけをSpark側で数える形になります。ただしDynamoDB側の課金対象はフィルタ前の読み込み量である点は通常のScan同様です。
Spark SQLの--whereで条件つきcount
countは--whereにSpark SQLの構文で複雑な条件を書けます。デフォルトジェネレータは3種類のアイテムを1バッチで返し、うち1つだけがpayload属性を持つ設計になっているため、payload IS NOT NULLで絞ると 約1/3 がヒットするはずです。
$ ./bulk count --table bulk-demo-202607 --where "payload IS NOT NULL" --XWaitForDPU
Running action 'count' with arguments: {'verb': 'count', 'table': 'bulk-demo-202607', 'where': 'payload IS NOT NULL', 'XWaitForDPU': True}
...
Starting Bulk Executor Glue Job...
Bulk Executor Glue Job started with job run ID: jr_8c2c15c98eb95c6fb59711dc2bfb595e77671a478e5c8d26caf06ad094816caa
Job start time: 2026-07-17 17:31:24.461771
...
Job completed successfully. Job duration: 0:01:38 (0.86 DPU hours)
...
[bulk-demo-202607] Max read rate set to account quota limit: 40000
Count of matching items: 333,300
333,300件 がヒット。100万件のほぼ1/3という期待通りの結果です(100タスク × 各1万件の内訳で、各バッチ内で3,333件がpayload付きに割り当てられる構造上、payload IS NOT NULLは 333,300 になります)。所要時間は1分38秒、DPU時間は0.86。Spark SQL側で条件を書けるため、DynamoDBのFilterExpression構文では表現できない a > b(属性間比較)や payload LIKE 'foo%' のような条件も書ける柔軟性があります。ただし演算子の前後には空白が必要な点だけ注意が必要です。
find --orderbyで並び替えて上位N件
findは条件に一致するアイテムを標準出力に最大10件出し、超過分はS3に書き出します。--orderbyで並び替えができ、GSIを作らずとも任意の属性をSpark側で全件ソートできるのが便利な点です(DynamoDBの範囲キーではなくSpark DataFrameのorderByで並べているため、任意の属性が指定できます)。
$ ./bulk find --table bulk-demo-202607 --orderby sk --limit 5 --XWaitForDPU
Running action 'find' with arguments: {'verb': 'find', 'table': 'bulk-demo-202607', 'orderby': 'sk', 'limit': 5, 'XWaitForDPU': True}
...
Starting Bulk Executor Glue Job...
Bulk Executor Glue Job started with job run ID: jr_b659d776dcea832cfeaa103659b5790a8b3868da786b4235588632568bdf1847
Job start time: 2026-07-17 17:27:01.568679
...
Job completed successfully. Job duration: 0:01:44 (0.86 DPU hours)
...
5 matching items:
{"pk":"sykxuyok7xkn","sk":"0005jbh047i9zj3e2uuc3m3k"}
{"pk":"7pp6ktfvpniv","payload":"iecq6msn54xqiy6n2sefdrh4kitfdu1k42vv20u7l21cdc7kv8d726b3ohsyhq973b3fohzyn2yrmk93y221hrj6tiqu7cbzyl7un5l6vtfj61rkb1jyuhn3at03anotbdc38vfqd68ncn06y8d0qrqdd6i8db5fokvsjfdmpk09duuoq8q8zjh60xeslrrtq9ak654u","sk":"0007x9tp5kjt9mhgswpno5ah"}
{"pk":"shr4g0m6w5ve","#meta":"1hefbof557","sk":"000bcuqybiny543u75xk212d"}
{"pk":"t8u9672s629l","sk":"000d1r6ntki87dl4hg0s1fju"}
{"pk":"6hklcoiew3uw","payload":"nvhg53c54vn9aghwxn6c6ypwbfa2wcciz5cw4tasdbahcj82ts82llvlp3op6h4pxjete31wjm6pfeeoezra2ndko28tjqzie07uj7nv5mx1mayh7jlwe8a7o1a7wlg2lox8z3n7r44byw3p3h9dvu9kefd6s799gv23xtbcr20zxmyw21ugofhk9r368edpu7h9q58n","sk":"000eap94gmmyjdhf9g0tq3tf"}
Wrote 5 items in JSON format to s3://aws-glue-bulk-dynamodb-ap-northeast-1-123456789012-bn2aieu6r/output/jr_b659d776dcea832cfeaa103659b5790a8b3868da786b4235588632568bdf1847/
sk昇順で5件が抽出できました。所要時間1分44秒、DPU時間0.86。Spark側でソートするため大きなオーダーだとメモリが必要ですが、その場合は--XWorkerTypeをG.2X以上に上げることで対処できます。JSON出力はコンソールに加えてS3上のジョブ出力プレフィックスにも保存されるので、後段のパイプラインに繋ぐことも可能です。
--orderbyはdesc修飾子や複数属性のカンマ区切りにも対応しています。ただし複数属性を渡す場合はシェル上で1引数になるように 必ずクォート する必要があります(クライアント側のfind.pyのvalidate_orderby()が col または col asc/desc の最大2トークンしか受け付けないためです)。
# タイムスタンプで新しい方から100件
$ ./bulk find --table bulk-demo-202607 --orderby timestamp desc --limit 100
# 複数属性でソートするときはクォートで1引数にまとめる
$ ./bulk find --table bulk-demo-202607 --orderby "pk asc, sk desc" --limit 100
# 演算子の前後の空白は必須。Spark SQLの構文が使える
$ ./bulk find --table bulk-demo-202607 --where "payload LIKE 'iecq%'" --limit 10
今回試したverbの実測サマリ
デフォルト(220 workers, G.1X)で回した結果を並べておきます。
| verb | 対象データ量 | 所要時間 | Glue DPU時間 | 補足 |
|---|---|---|---|---|
fill |
100万件書き込み | 5:48 | 2.00 | On-Demand書き込み$0.72(見積) |
scancount |
100万件カウント | 2:07 | 0.87 | フルスキャン |
count --where "payload IS NOT NULL" |
333,300件抽出 | 1:38 | 0.86 | Spark SQLフィルタ |
find --orderby sk --limit 5 |
上位5件取得 | 1:44 | 0.86 | S3にも書き出し |
読み取り系の3コマンドはいずれも約1〜2分・0.86〜0.87 DPU時間で、100万件規模ならGlueジョブの起動オーバーヘッドが大部分を占めていると読み取れます。「小さいテーブルなら--XNumberOfWorkersを減らしてコスト削減」というREADMEのアドバイスは、この計測結果を見るとまさに納得できます。
試していないが記事執筆時点で押さえておきたいverb
今回試したverb以外にも、実業務でよく使われるものについて要点だけ整理しておきます。
update:既存アイテムの一括更新
「各アイテムを見て必要ならUpdateExpressionを返す」タイプのジェネレータスクリプトをそのままGlue側で並列適用してくれます。updateは--whereを取らないため、絞り込みロジックはジェネレータ内に書く必要があります。
$ ./bulk update --table mytable --generator touched
diff:2テーブルの差分
セグメント化されたスキャンで2つのテーブルを比較して差分を出力します。--format fullにすると変更前後の全体像が出るため、copy後の差分検証にも使えます。
$ ./bulk diff --table t --table2 t2
$ ./bulk diff --table t --table2 t2 --format full --s3
$ ./bulk diff --table t --table2 t2 --sample-fraction 0.1
copy:テーブル間コピー
Spark側にテーブル全体を載せず、tight scan/writeループで並列コピーを行います。ソースとターゲットにARNを渡せばクロスアカウント/クロスリージョンのコピーもリソースベースポリシー次第で可能です。
$ ./bulk copy --source source --target target
$ ./bulk copy \
--source arn:aws:dynamodb:us-east-1:123456789012:table/source \
--target arn:aws:dynamodb:us-west-2:987654321098:table/target
load-export / revert-export:DynamoDBエクスポート連携
load-exportはS3にエクスポートしたDynamoDB(DDB-JSON形式のフル/差分エクスポート)を別テーブルに流し込みます。revert-exportは差分エクスポートの内容を打ち消す方向で 同一テーブルにin-placeで書き戻し 、「特定時間帯の書き込みだけを取り消す」といった巻き戻しに使えます。ただしrevert-exportはエクスポート時のビュー設定がNEW_AND_OLD_IMAGESのものしか対応しておらず、それ以外はfail fastで停止します。
$ ./bulk load-export --table target --s3-path "s3://<bucket-name>/prefix/AWSDynamoDB/01716790307109-5f9d6aaa" \
--XMaxWriteRate 40000 --XTimeout 720
$ ./bulk revert-export --table target --s3-path "s3://<bucket-name>/prefix/AWSDynamoDB/01716790307109-5f9d6aaa"
Glue実行の細かい調整
--Xから始まるオプションでGlueジョブの挙動を調整できます。今回試して有用だったものと、READMEに載っているものを合わせて表にまとめておきます。
| オプション | 説明 |
|---|---|
--XExecutionClass |
STANDARD(既定) / FLEX(空き容量利用でDPU単価が安いが遅くなる傾向) |
--XNumberOfWorkers |
Glueワーカー数(既定220)。小さいテーブルなら減らしてコスト削減できるのは実測で実感 |
--XWorkerType |
G.1X(既定) / G.2X / G.4X / G.8X / G.12X / G.16X / R.1X / R.2X / R.4X / R.8X。orderby等でメモリを大量に使う場合に上げる |
--XTimeout |
Glueジョブのタイムアウト(分)。既定60分、最大10080分(7日) |
--XMaxReadRate |
1秒あたりの読み取りユニット上限 |
--XMaxWriteRate |
1秒あたりの書き込みユニット上限 |
--XWaitForDPU |
実行終了時に40秒待ってDPUメトリクスを取得・表示。今回のように「所要時間・DPU時間・コスト」を把握したいときに必須 |
環境を撤去する
動作確認が終わったらteardownアクションでGlueジョブ・Glue接続・自動作成されたIAMロール・S3バケット(bootstrapで作成されたserver/配下を削除した後、バケットが空なら削除)が片付けられます。ただし後述するとおりS3上の出力オブジェクトやCloudWatchロググループ、--XRoleで指定したカスタムIAMロールはそのまま残ります。今回はfindで書き出したJSONがoutput/配下に残っていたので、そのままだとバケットが消えないパターンを実演できました。
$ ./bulk teardown
Running action 'teardown' with arguments: {'verb': 'teardown'}
Deleted 3 objects from aws-glue-bulk-dynamodb-ap-northeast-1-123456789012-bn2aieu6r
2026-07-17 18:19:32,554 WARNING [MainThread] root - Bucket aws-glue-bulk-dynamodb-ap-northeast-1-123456789012-bn2aieu6r is not empty and could not be deleted. Continuing with teardown.
Detaching managed policy 'AWSGlueServiceRole'...
Detaching managed policy 'AmazonDynamoDBFullAccess'...
Deleting inline policy 'MinimalPricingAccess'...
Deleting inline policy 'MinimalQuotasAccess'...
Bulk Executor Glue Job Role deleted: AWSGlueServiceRoleBulkDynamoDB-DdbReadWrite-ap-northeast-1
Bulk Executor Glue Job deleted successfully.
Deleted Glue connection 'bulk-dynamodb-connection'.
Bulk Executor environment configurations:
AWS -
Account: 123456789012
Region: ap-northeast-1
Role: arn:aws:sts::123456789012:assumed-role/xxxxxxx/botocore-session-xxxxxxxxxx
READMEにも記載があるとおり、teardownはS3上のジョブ出力(findやdiffで書き出したファイルなど)は削除しないため、上の実行例ではoutput/配下のJSONがまだ残っており「Bucket is not empty and could not be deleted」の警告を出しつつも他リソースは全て片付けています。S3バケットも合わせて完全に消したい場合はaws s3 rm --recursive → aws s3 rbで明示的に削除してください。
$ aws s3 rm s3://aws-glue-bulk-dynamodb-ap-northeast-1-123456789012-bn2aieu6r --recursive
delete: s3://aws-glue-bulk-dynamodb-ap-northeast-1-123456789012-bn2aieu6r/output/jr_.../part-00000-....json
$ aws s3 rb s3://aws-glue-bulk-dynamodb-ap-northeast-1-123456789012-bn2aieu6r
remove_bucket: aws-glue-bulk-dynamodb-ap-northeast-1-123456789012-bn2aieu6r
$ aws dynamodb delete-table --table-name bulk-demo-202607 --region ap-northeast-1
teardown後にリソースが本当に消えているかも確認できます。
$ aws glue get-job --job-name bulk_dynamodb --region ap-northeast-1
An error occurred (EntityNotFoundException) when calling the GetJob operation: Job not found.
$ aws s3 ls | grep aws-glue-bulk-dynamodb
$ aws iam list-roles \
--query 'Roles[?starts_with(RoleName,`AWSGlueServiceRoleBulkDynamoDB`)].RoleName' \
--output text
CloudWatchロググループ(/aws-glue/jobs/error, /aws-glue/jobs/output)はteardownでは削除されないため、料金や保存期間が気になる場合は手動で削除するか、保存期間を短くしておくとよいでしょう。
まとめ
DynamoDBの大規模テーブルに対する一括処理を、CLIライクな操作感で行える Bulk Executor for Amazon DynamoDB を試してみました。bootstrapで必要なリソースをまとめて作り、Sparkジョブを自分で書かなくてもfill・scancount・count --where・find --orderbyといった実務でよく使う一括操作がそのまま叩けるのは便利でした。書き換え系はPITR必須という安全装置もあるので、間違って本番テーブルを壊すリスクが減らせます。
DynamoDBのデータ移行、バックフィル、テストデータ投入、テーブル間コピーなど、都度Glueジョブを組む手間を大きく減らせるツールなので、機会があれば活用していきたいと思います。
最後まで読んで頂いてありがとうございました。








