DynamoDBが簡単にExportできるようになった！

本記事は[ハンズラボ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/handslab)の17日目です。

#はじめに！
こんにちは。AWS絶賛勉強中の@bassaaaaaです。

業務でDynamoDBテーブルを全件取得し、ゴニョゴニョする必要がありました。
RDBなら普通にExportできますが、DynamoDBは最近までできなかったみたいです。
というわけで、最近出たDynamoDB Export[^1]をaws-cliからやってみたいと思います。

前置きはいいから！という人は[aws-cliでDynamoDB Exportをやってみる](#aws-cliでdynamodb-exportをやってみる)へ

#昔は大変だった！
DynamoDB Exportは2020/11/9から使えるようになりました。
それまでは以下のような方法でExportを行う必要がありました。[^2]

1. AWS Data Pipelineを使う
1. Amazon EMRを使う
1. AWS Glueを使う
1. 全件取得クエリ

1~3はなんとなく、面倒そうです。
4は一発で終わり簡単そうです。
これらと比べて何が良いんでしょうか？？

#いろいろ心配しなくていい！
公式ドキュメント[^3]にはこのように書かれています。
>Exporting a table does not consume read capacity on the table, and has no impact on table performance and availability. 
テーブルをエクスポートしても読み込みキャパシティは消費されず、テーブルのパフォーマンスや可用性に影響はありません。

#どういうこと？
DynamoDBはデータの読み書きに :moneybag: がかかります。
その読み書き性能はテーブル毎に設定するキャパシティユニットにより決まります。
値が大きいほど、性能が良く:moneybag: もかかります。

* 読み取りキャパシティユニット（RCU）
    * 1RCUは1秒間に４KBまでの項目を1回読み取れます。[^4]
* 書き込みキャパシティユニット（WCU）
    * 1WCUは1秒間に1KBまでの項目を1回書き込めます。[^4]

100RCUなら1秒間に4KB*100個の項目を同時に読み取れますよー、みたいな感じです。
この性能を超えたリクエストが来た場合、スロットリングエラーが発生し、読み込み/書き込みリクエストが失敗します。

これは、上記1~4の方法でも起きる可能性があります。
Exportが失敗するのは別に良いとして、Exportが食い潰したキャパシティにより、本来成功したはずの本番処理が失敗するのは避けたいです。

なので、上記1~4の方法でExportする場合は、**事前にキャパシティをあげ、Export中はスロットルエラーが発生してないか監視する必要があった**のです。
また、キャパシティをあげるため:moneybag:もかかり、上記リスクもあるので、
**本当にExportする必要があるのか？など、要らぬ心配も必要だった**はずです（ただのExportなのに）

DynamoDB Exportはここら辺の心配をしなくて良いのです:tada: :confetti_ball: 

#DynamoDB Exportの仕組み
DynamoDB Exportは内部的に__PITR (Point In Time Recovery)__ を使用しています。
PITRはDynamoDBテーブルの自動バックアップ機能で、過去35日間の任意の時点（1秒単位）へ復元できます。
というわけで、DynamoDB Export使用前にPITRを有効化する必要があります。

ExportデータはDynamoDB JSON形式かION形式のgz圧縮でS3に保存されます。
出力ディレクトリ構造は以下のような感じです。
Exportデータはサイズによって複数分割されることもあるみたいです。

```
s3://
  └ hogehoge                                       # バケット名（指定）
    └ fugafuga                                     # prefix（指定）
      └ ...                                        # prefix（指定）
        └ AWSDynamoDB                              # 固定
          └ 01234567890abc-1234abc                 # ランダム文字列
            ├ ...                                  # manifestなど
            └ data                                 # 固定
              └ abcdefghijklmnopqrstuvwxyz.json.gz # Exportデータ(JSON or IONを指定)
```

#aws-cliでDynamoDB Exportをやってみる
DynamoDB Exportはマネジメントコンソールからでもできます。
「どのテーブルをどのバケットに」を指定するだけなので、簡単です。
が、今後のために一連の処理をaws-cliを使ってshellで書いてみます。

###実行環境

* macOS Catalina (10.15.7)
* aws-cli/2.1.5

※ 本番環境でのaws-cliのVerUpは要注意

###ざっくり流れ
* PITRを有効化する
* Exportリクエスト
* Export状況問合せ
* PITRの設定を元に戻す
* S3からExportデータをダウンロードする

以下、変数など適当に補完してください

###PITRの有効化

``` bash:dynamodb_export.bash
# PITR設定取得
aws dynamodb describe-continuous-backups \
    --table-name $TABLE_NAME > $tmp-init-setting

# 設定値取得
cat $tmp-init-setting | jq -r ".ContinuousBackupsDescription.PointInTimeRecoveryDescription.PointInTimeRecoveryStatus" > $tmp-init-status

# 有効でない場合は有効にする
if [ "$(cat $tmp-init-status)" = "DISABLED" ]; then
    aws dynamodb update-continuous-backups \
        --table-name $TABLE_NAME \
        --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true > $tmp-changed-setting
fi
```
レスポンス

```json:$tmp-init-setting
{
    "ContinuousBackupsDescription": {
        "ContinuousBackupsStatus": "ENABLED",
        "PointInTimeRecoveryDescription": {
            "PointInTimeRecoveryStatus": "DISABLED"
        }
    }
}
```

```json:$tmp-changed-setting
{
    "ContinuousBackupsDescription": {
        "ContinuousBackupsStatus": "ENABLED",
        "PointInTimeRecoveryDescription": {
            "PointInTimeRecoveryStatus": "ENABLED",
            "EarliestRestorableDateTime": "2020-12-07T15:06:44+09:00",
            "LatestRestorableDateTime": "2020-12-07T15:06:44+09:00"
        }
    }
}
```

###Exportリクエスト
```bash:dynamodb_export.bash
# DynamoDB Exportリクエスト
# TABLE_ARN  arn:aws:dynamodb:REGION:ACCOUNT:table/tablename
# s3://S3_BUCKET/S3_PREFIX
# EXPORT_FORMAT  DYNAMODB_JSON / ION
aws dynamodb export-table-to-point-in-time \
    --table-arn $TABLE_ARN \
    --s3-bucket $S3_BUCKET \
    --s3-prefix $S3_PREFIX \
    --export-format $EXPORT_FORMAT > $tmp-request

# ExportArnを取得
export_arn=`cat $tmp-request | jq -r '.ExportDescription.ExportArn'`

# Export先S3ディレクトリ名を取得
# ExportArnの最後の/以降
export_dir=`cat $export_arn | sed -e "s#\(.*\)\/\(.*\)#\2#"`

```

レスポンス

```json:$tmp-request
{
    "ExportDescription": {
        "ExportArn": "arn:aws:dynamodb:REGION:ACCOUNT:table/tablename/export/01234567890abc-1234abc",
        "ExportStatus": "IN_PROGRESS",
        "StartTime": "2020-12-04T10:46:23.577000+09:00",
        "TableArn": "arn:aws:dynamodb:REGION:ACCOUNT:table/tablename",
        "TableId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "ExportTime": "2020-12-04T10:46:23.577000+09:00",
        "ClientToken": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "S3Bucket": "hogehoge",
        "S3Prefix": "fugafuga",
        "S3SseAlgorithm": "AES256",
        "ExportFormat": "DYNAMODB_JSON"
    }
}

```

###Export状況問合せ

```bash:dynamodb_export.bash
# Export状況取得
while :
do
    # ステータスがCOMPLETEDになるまで60秒ごとに問い合わせる
    aws dynamodb list-exports | 
    jq -r ".ExportSummaries | map(select(.ExportArn == \"$export_arn\")) | .[].ExportStatus" > $tmp-status

    cat $tmp-status
    if [ "$(cat $tmp-status)" = "COMPLETED" ]; then
    	break
    fi
    sleep 60
done

```

###PITRの設定を元に戻す

```bash:dynamodb_export.bash
if [ "$(cat $tmp-init-status)" = "DISABLED" ]; then
    aws dynamodb update-continuous-backups \
        --table-name $TABLE_NAME \
        --point-in-time-recovery-specification PointInTimeRecoveryEnabled=false > $tmp-last-setting
fi
```

###S3からExportデータをダウンロードする

```bash
# S3からコピーする
aws s3 cp s3://$S3_BUCKET/$S3_PREFIX/AWSDynamoDB/$export_dir/data/ ./ --recursive
```

###Export結果
Exportデータはこんな感じです。

```json
{"Item": {"id": {"S": "01"},"name": {"S": "fuga"}}}
{"Item": {"id": {"S": "02"},"name": {"S": "hoge"}}}
...
```
### おまけ
DynamoDB JSON --> CSV

```bash
cat export.json | jq -r ".Item | [.id.S, .name.S] | @csv"
"01","fuga"
"02","hoge"
```

##（参考）Export時間
いくつかのテーブルでExport時間を見てみました。
Export自体は1.5〜４分で終わりました。実際には、リクエストしてExportが開始するまでの時間がかかるので、5〜10分以内といった感じです。

| テーブル | 項目数 | テーブルサイズ | Export時間|
|:-:|:-:|:-:|:-:|
| 1 | 4  | 25B  |  1.5〜4分（複数回実施） |
| 2 | 3万  | 6MB  |  2分 |
| 3 | 120万  | 180MB  | 2分  |
| 4 | 200万  | 300MB  | 2分5秒  |

AWSブログ[^1]によると、
>完了時間は、テーブルのサイズと、テーブル内でデータがどのように均一に分散されているかに左右されます。エクスポートの大部分は 30 分以内に完了します。10 GiB までの小さなテーブルの場合、数分で完了します。テラバイトのオーダーの非常に大きなテーブルの場合、数時間かかる場合があります。

らしいです。

#お金 :moneybag: のお話
上で「*いろいろ心配しなくて良い*」と書きました。
確かにキャパシティユニットとか本番への影響は心配しなくて良いです。
ただ、やっぱりお金はかかります。
具体的には、下記3つです。

* PITRのお金
    * 毎月 0.228USD/GB　~~（月中に一回でも有効化すれば課金？）~~有効化してた時間のみ課金[^5]
* DynamoDB Exportのお金
    * 0.114USD/GB
* Export先S3バケットのお金

ただ、なんだかんだ楽ですし、以前までの方法と比べると安いもんです。



#最後に
DynamoDB ExportはDynamoDBとS3だけで完結してるのが良いですね。
RDBのExport並みにお手軽です。


最後までお読みいただきありがとうございました。

[^1]: 「新機能 – Amazon DynamoDB テーブルデータを Amazon S3 のデータレイクにエクスポート。コードの記述は不要」(https://aws.amazon.com/jp/blogs/news/new-export-amazon-dynamodb-table-data-to-data-lake-amazon-s3/)
[^2]: 「DynamoDB テーブルを Amazon S3 にバックアップするにはどうすればよいですか ?」 (https://aws.amazon.com/jp/premiumsupport/knowledge-center/back-up-dynamodb-s3/)
[^3]: 「Exporting DynamoDB table data to Amazon S3」(https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DataExport.html)
[^4]: 実際は色々モードあり。詳細は「読み込み/書き込みキャパシティーモード」( https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html)
[^5]: PITRの料金は「その月のPITRを有効化していた時間の1時間あたりの平均テーブル容量(GB-Month)×料金表に記載の単価」です。東京リージョンで10GBのテーブルをExportのため1時間だけPITRを有効化した場合の料金は、((10GB * 1h * 1日) / (24h * 30日)) * 0.228USD/GB = 0.003USDとなります。