BatchWriteItemを利用してAmazon DynamoDBのテーブルを空にする

こんにちは。サービスグループの武田です。

DynamoDBを使用しているシステムのテストや動作検証をしていると、一旦テーブルの内容をクリアしたいことがあるかと思います。

DynamoDBのテーブルを削除して再作成ができれば早いんですが、今回は以下のようなケースで再作成が難しい状況でした。

  1. CloudFormationで作成したリソースのため手動での再作成は避けたかった
  2. テーブル名の一部にランダム値が入りスタックの再作成はやりたくなかった
    • 別アプリケーションの設定ファイルにテーブル名がハードコーディングされていた
  3. 本番環境構築にも使用しているテンプレートで 正直触りたくなかった 変更が難しかった

さて、デーブルの再作成ができないとなると、データを一つ一つ消していくという手段になります。

実はAWS コンソールでは100件ずつではありますが、一括削除することができます。

dynamo_delete

最初はこの方法で削除してたんですが、データを削除したいテーブルが複数個あったのも災いして、何回か繰り返しているうちにめんどくさくなってきてしまい、楽したいなと思い始めました。

というわけで以下の要求を解決するべくスクリプト作成に取りかかりました。

  • ブラウザに切り替えずに削除したい
  • 100件以上でも一発で削除したい
  • 複数テーブルのデータを楽に削除したい
  • 楽したい

動作環境

動作確認は以下の環境で行なっています。

  • macOS Sierra
  • AWS CLI
  • jq
$ sw_vers -productVersion
10.12.6
$ aws --version
aws-cli/1.11.180 Python/3.6.3 Darwin/16.7.0 botocore/1.7.38
$ jq --version
jq-1.5

削除スクリプトの作成

DynamoDBではDeleteItemという操作でデータを一つ削除することができます。つまり全データを削除するためには、データ一つ一つに対してDeleteItemを実行すれば良いことになります。

……なんですが、実はBatchWriteItemという操作を使うと25件ずつまとめて削除することができます。バッチ操作を利用することでネットワークラウンドトリップを減らすことができ、さらにDynamoDB側で並行して書き込み処理をしてくれて大変便利です。

初めはDeleteItemを使ったスクリプトを書いたんですが、「BatchWriteItem使った方がきっとパフォーマンス良くなるよ!」というアドバイスをもらい書き換えました。

ということでこんなスクリプトになりました。

dynamodb_truncate.sh

#!/bin/bash

set -e

prefix=$1
tables=$(aws dynamodb list-tables | jq -r '.[][] | select(. | test("^'$prefix'-.+"))')

tmp_scan_data=$(mktemp)
tmp_delete_items=$(mktemp)

for t in $tables; do
  key=$(aws dynamodb describe-table --table-name $t | jq -r '.Table.KeySchema[].AttributeName')

  # 予約語はprojection-expressionで指定できないため、全て#を付与してしまう
  proj=$(echo -n $key | tr ' ' '\n' | sed -E 's/(.+)/#\1/' | tr '\n' ',')
  attr=$(echo -n $key | tr ' ' '\n' | sed -E 's/(.+)/"#\1":"\1"/' | tr '\n' ',' | sed -E 's/(.+)/{\1}/')

  while :; do
    aws dynamodb scan --table-name $t --projection-expression "$proj" --expression-attribute-names "$attr" --max-item 25 > $tmp_scan_data

    count=$(cat $tmp_scan_data | jq '.Count')
    if [[ $count -eq 0 ]]; then
      echo "${t}: delete completed."
      break
    fi

    echo "${t}: delete progress ... ${count}."

    cat $tmp_scan_data | jq '.Items[] | {"Key": .} | {"DeleteRequest": .}' | jq -s '.' | jq '{"'$t'": .}' > $tmp_delete_items

    aws dynamodb batch-write-item --request-items file://$tmp_delete_items > /dev/null
  done
done

以下のように実行します。

./dynamodb_truncate.sh hoge

そうするとhoge-*のテーブル全てのデータを削除することができます。

これで手軽にテーブルデータを空にすることができるようになりました。

作成中に詰まったポイント

スクリプトを作成する中で詰まったポイントを紹介します。

キー以外の情報を含めてしまう

DeleteItemでもBatchWriteItemでも同様なんですが、削除するデータを指定する際にはキー情報のみを指定します。

そうしないと

An error occurred (ValidationException) when calling the BatchWriteItem operation: The number of conditions on the keys is invalid

というエラーが出て怒られます。

スクリプトでは、scanする際に--projection-expressionオプションを指定することでキーの値のみを取得するようにしています。

aws dynamodb scan --table-name $t --projection-expression "$proj" --expression-attribute-names "$attr" --max-item 25 > $tmp_scan_data

テーブルによってキーが違う

キー情報のみを取り出すためには、キー名を指定する必要があります。ところが、当たり前ですがテーブルによってプライマリキーは異なります。そのためスクリプト内でキー名を指定することができません。

メタ情報から取ってくればいけるかな?ということで、スクリプトではテーブルのメタ情報からキー名を取得するようにしています。

またプライマリキーの数も1つのテーブルと2つのテーブルがあることに注意が必要でした。

key=$(aws dynamodb describe-table --table-name $t | jq -r '.Table.KeySchema[].AttributeName')

キー名に予約語を使っている

--projection-expressionオプションでキー名のみを指定すると前述しましたが、DynamoDBの予約語を直接指定すると以下のようなエラーが返されます(予約語timestampを使っていた場合)。

An error occurred (ValidationException) when calling the Scan operation: Invalid ProjectionExpression: Attribute name is a reserved keyword; reserved keyword: timestamp

対応策としては、--projection-expressionには#から始まるプレースホルダーを指定し、プレースホルダーと属性名のマッピング情報を--expression-attribute-namesで指定してあげます。

スクリプトではキー名に一律で#を付与することでエラーが起きないようにしています。

proj=$(echo -n $key | tr ' ' '\n' | sed -E 's/(.+)/#\1/' | tr '\n' ',')
attr=$(echo -n $key | tr ' ' '\n' | sed -E 's/(.+)/"#\1":"\1"/' | tr '\n' ',' | sed -E 's/(.+)/{\1}/')

...

aws dynamodb scan --table-name $t --projection-expression "$proj" --expression-attribute-names "$attr" --max-item 25 > $tmp_scan_data

まとめ

DynamoDBではテーブルのデータを空にしたい場合、まず考えるべきことはテーブルの再作成です。ところが諸々の事情で、テーブルの再作成をせず、データだけ消したいということもあると思います。そのような場合に少しでも役立てられたら嬉しいです。

最後に注意点ですが、Scanは読み込みキャパシティーユニット(RCU)を、BatchWriteItemは書き込みキャパシティーユニット(WCU)を消費します。そのため、上記のスクリプトによる処理速度はテーブルに設定されているスループットキャパシティーと実際に削除するデータ量に依存します。

RCUとWCUが1に指定されているテーブルでデータ削除の検証をしてみたところ、100件の削除に10秒程度、1000件の削除に12分弱かかりました。数千レコード程度のオーダーであれば大丈夫そうですね(ちなみにDeleteItemバージョンでは100件の削除に1分30秒ほどかかっていました)。

一方で、データ量が多く削除時間が無視できない場合には、テーブル再作成できないかを考慮した方が良さそうです。

それでは、よいDynamoライフを!!