Lambdaを使わずにDDBからS3へデータを出力する

# はじめに
この記事はミロゴス Advent Calendar 2023 2日目の投稿です。

弊社ではデータをDynamoDBにストアすることが多く、そのデータを活用するにあたって、CSVファイルをS3に置くためのLambdaを実装、定期的に実行しています。今回、DDBのS3エクスポート機能を使うことで実装をなくし、さらに定期的な実行とそのステータス管理のためにStep Functionsで処理をしていきます。

ステートマシンの完成図は下記のような形です。

![ステートマシン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224198/922031b1-5780-5923-b7e3-e872af625c11.png)

# DDBからS3へのエクスポート

LambdaでDDBのデータを出力するにはテーブルに対して、ScanやQueryを行う必要があります。これらにはRead Capacity Unitが消費されるため、コストやパフォーマンス、可用性に影響が出てしまいます。それに対して、[DDBのS3エクスポート機能はRead Capacityを消費せずに利用できます](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/S3DataExport.HowItWorks.html)。

このS3エクスポート機能を利用するには下記の条件、制限があります。

- DDBのテーブルにPoint In Time Recoveryの設定が有効化されていること。
- 出力する形式はサポートされているもののみに制限される。
  - CSVはサポートされていません（2023/11/27現在）。

さらに今回はStep Functionsで実行するため、ステートマシンがS3エクスポートを実行できるように下記の権限がStep FunctionsのIAM Roleに必要です。

- s3:AbortMultipartUpload
- s3:PutObject
- s3:PutObjectAcl

## サポートされている出力形式

S3エクスポート機能では出力の形式としてCSVはなく、[DynamoDB JSONかAmazon Ionになります](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/S3DataExport.Output.html)。今回は出力したデータをTreasureDataなど複数のシステムが利用することを考慮し、Amazon固有のIonは辞め、[JSON Lines](https://jsonlines.org/)で定義される.json.gzで出力されるDynamoDB JSONを選択しました。なお、`.jsonl.gz`ではなく、`.json.gz`になります。

また、出力内容に関しては全量データと増分データがサポートされています。[増分データのサポートは2023/09に新たにサポートされました](https://aws.amazon.com/jp/about-aws/whats-new/2023/09/incremental-export-s3-amazon-dynamodb/)。

この投稿では全量データをDynamoDB JSONの形式で出力していきます。

なお、増分データの出力の場合、出力のFromとToを指定する必要があります。
Step Functionsだけではそれらのデータを保持できないので、別途DDBなどのデータストアを利用する作りを考える必要があります。DDBを使った方法は下記が参考になるかと思います。

[DynamoDB Incremental Export with Step Functions](https://www.binaryheap.com/dynamodb-incremental-export-step-functions/)

## エクスポートの実行

コンソールからの操作も可能ですが、今回は[AWS CLIを使います](https://docs.aws.amazon.com/cli/latest/reference/dynamodb/export-table-to-point-in-time.html)。

```bash
aws dynamodb export-table-to-point-in-time \
--table-arn [エクスポートするDDBテーブルのARN] \
--s3-bucket [エクスポート先のバケット名] \
--export-format DYNAMODB_JSON --export-type FULL_EXPORT \
--s3-prefix [バケットオブジェクトのプレフィックス]

{
    "ExportDescription": {
        "ExportArn": "arn:aws:dynamodb:ap-northeast-1:xxxxxxxxxxxx:table/test/export/xxxxxxxxxxxxxx-xxxxxxxx",
        "ExportStatus": "IN_PROGRESS",
        "StartTime": "2023-11-27T15:07:58.449000+09:00",
        "TableArn": "arn:aws:dynamodb:ap-northeast-1:xxxxxxxxxxxx:table/test",
        "TableId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "ExportTime": "2023-11-27T15:07:58.449000+09:00",
        "ClientToken": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "S3Bucket": "hoge.bucket",
        "S3Prefix": "aaa",
        "S3SseAlgorithm": "AES256",
        "ExportFormat": "DYNAMODB_JSON"
    }
}
```

export-formatオプション以降は任意です。設定しない場合はデフォルトでフォーマットはDYNAMODB_JSON、エクスポートタイプはFULL_EXPORTになります。
また、s3-prefixを指定することでエクスポート先のバケットに対して、オブジェクトのプレフィックスを切ることができます。
ただし、下記のエラーが出るため指定するプレフィックスは3文字以上の必要があります。

> An error occurred (ValidationException) when calling the ExportTableToPointInTime operation: Invalid Request: 1 validation error detected: Value 'aa' at 's3Prefix' failed to satisfy constraint: Member must have length greater than or equal to 3

## エクスポート処理の確認

エクスポート処理は[DescribeExport API](https://docs.aws.amazon.com/cli/latest/reference/dynamodb/describe-export.html)で処理を確認できます。

```bash
aws dynamodb describe-export --export-arn [ExportArn] | jq .ExportDescription.ExportStatus

"COMPLETED"
```

ステータスはIN_PROGRESS（処理中）と COMPLETED（完了）、 FAILED（失敗）があります。

# 定期的にエクスポートする

Step FunctionsはSDK統合によって、[多くのAWSサービスをサポートしています](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/supported-services-awssdk.html#supported-services-awssdk-list)。DDBからS3へのエクスポート機能もこの形でサポートされています。

サービス統合には３パターンありますが、DDBのエクスポート処理はAWS SDK統合になるため[リクエストレスポンスか、コールバックまで待機](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/connect-supported-services.html)のどちらかになります。今回はリクエストレスポンスを用います。

作成したステートマシンに対して、定期的な起動にはEventBridge Schedulerをcron設定して実行させます。

## ステートマシンを作成する

前項で実施していたエクスポート処理を実行し、60秒間の待ち状態の後、処理のステータス確認をし、ステータスがIN_PROGRESSなら、待ち状態に戻るようにループを実施します。また、エクスポート処理が失敗した場合、Failedステートに遷移させます。

下記がそのASLの内容です。

```json
{
  "Comment": "A description of my state machine",
  "StartAt": "ExportTableToPointInTime",
  "States": {
    "ExportTableToPointInTime": {
      "Type": "Task",
      "Parameters": {
        "S3Bucket": "[エクスポートするDDBテーブルのARN]",
        "TableArn": "[エクスポート先のバケット名]",
        "ExportFormat": "DYNAMODB_JSON",
        "ExportType": "FULL_EXPORT"
      },
      "Resource": "arn:aws:states:::aws-sdk:dynamodb:exportTableToPointInTime",
      "Next": "Wait Export Task"
    },
    "Wait Export Task": {
      "Type": "Wait",
      "Seconds": 60,
      "Next": "DescribeExport"
    },
    "DescribeExport": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:dynamodb:describeExport",
      "Parameters": {
        "ExportArn.$": "$.ExportDescription.ExportArn"
      },
      "Next": "Choice Export Status"
    },
    "Choice Export Status": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.ExportDescription.ExportStatus",
          "StringMatches": "COMPLETED",
          "Next": "Success"
        },
        {
          "Variable": "$.ExportDescription.ExportStatus",
          "StringMatches": "IN_PROGRESS",
          "Next": "Wait Export Task"
        }
      ],
      "Default": "Fail"
    },
    "Success": {
      "Type": "Succeed"
    },
    "Fail": {
      "Type": "Fail"
    }
  }
}
```

ASLは自分で書かず、Step Functions Studioで作成したものをJSONの形式でエクスポートしています。

![Step Functions Studio](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224198/ce82cccd-9ce7-a6f4-0ebd-75fa66dce86c.png)

エクスポート処理が完了すると、指定したprefixを付与してエクスポート先バケットに出力データが配置されます。

```bash
aws s3 ls --recursive s3://[エクスポート先のバケット名]

2023-11-27 15:08:17          0 aaa/AWSDynamoDB/01701065278449-9b3c467c/_started
2023-11-27 15:13:23       6955 aaa/AWSDynamoDB/01701065278449-9b3c467c/data/h7sjj2scwuyvnfcsfr3sxyfmg4.json.gz
2023-11-27 15:13:48        201 aaa/AWSDynamoDB/01701065278449-9b3c467c/manifest-files.json
2023-11-27 15:13:48         24 aaa/AWSDynamoDB/01701065278449-9b3c467c/manifest-files.md5
2023-11-27 15:13:48        711 aaa/AWSDynamoDB/01701065278449-9b3c467c/manifest-summary.json
2023-11-27 15:13:48         24 aaa/AWSDynamoDB/01701065278449-9b3c467c/manifest-summary.md5
```

`AWSDynamoDB`の後ろの`01701065278449-9b3c467c`はエクスポートIDになります。この値はエクスポート処理時に自動生成されるため、エクスポート結果は実行時のIDから紐づけて取得する必要があります。

定期的に動かした結果を実行ごとに取得したい場合、ExportTableToPointInTime APIのprefixを利用して日付でオブジェクトを分ける手があります。

なお、データ量によっては`.json.gz`ファイルは複数作成されることがあります。

```tsv
2023-11-20 17:25:45     926913 hoge/AWSDynamoDB/01700468343538-dec955b1/data/cntkk3twyy6xtlm73mwc5i5qzu.json.gz
2023-11-20 17:26:01     697238 hoge/AWSDynamoDB/01700468343538-dec955b1/data/dcb2leftwi63vgoo7skmnvyoby.json.gz
2023-11-20 17:25:54        189 hoge/AWSDynamoDB/01700468343538-dec955b1/data/djz4msstlu5nnbu5kmmxwn4ehi.json.gz
2023-11-20 17:25:53      70313 hoge/AWSDynamoDB/01700468343538-dec955b1/data/nf7a3ifiem2fhbjneqb3o6vpiy.json.gz
2023-11-20 17:25:28         20 hoge/AWSDynamoDB/01700468343538-dec955b1/data/x73h5vegve45lag535vfa5uvpe.json.gz
```

## ステートマシンを定期的に動かす

ステートマシンを定期的に実行するにあたっては、EventBridge Schedulerを利用します。

EventBridge SchedulerとRuleの違いや設定内容は昨年の投稿に書いていますので、こちらをご参考ください。

https://qiita.com/taskforce_Hisui/items/4c72b910c37b09da52ca#%E5%89%8D%E6%8F%90

# まとめ

サーバレス構成でのストア先として選ばれることの多いDDBからS3へデータを定期的にエクスポートする構成を作成しました。

DDBに蓄積されたデータを起点に別の作業、連携を行うことはあるかと思います。その際、この構成を使うことで実装なしでデータを出力できるようになりますので、参考にして頂けたらと思います。

次の私の投稿では、再利用性を高めるために弊社でいつも使っているCDKで今回の構成をコード管理させるようにしていく予定です。