【新機能】Amazon DynamoDB Table を S3 に Export して Amazon Athena でクエリを実行する

つい最近 Glue Job で全く同じ機能を実装したところです
2020.11.10

テーブルのデータを S3 へエクスポート

DynamoDB にエキサイティングな機能が追加されました。Data Export 機能です。この新機能を使えば、No Code で DynamoDB Table のデータを S3 に出力できます。

New – Export Amazon DynamoDB Table Data to Your Data Lake in Amazon S3, No Code Writing Required | AWS News Blog

ユースケースは?

そもそも DynamoDB は NoSQL データベースです。大量のデータを保存するワークロードに向いている一方、集計、走査、検索については効率の面で限界があります。では「DynamoDBに保存されたデータを集計して分析したい」という要件にどう応えるかというと、分析したいデータはとりあえずS3に入れておいてETLなりImportなりする という作戦があります。これまで、DynamoDBテーブルのデータをS3に出力するために様々な手法が編み出されてきました。

何が嬉しいの?

ざっくり、S3 Export に際して、考えることとやることが減ります。

  • No code, full magenged, full serverless, auto scaling である
  • Read Capacity を消費しないため、プロダクション環境のテーブルに対しても遠慮なくエクスポートを実行できる(すごい)
  • 時間単位ではなくデータサイズ課金で、コスト効率が良い。東京リージョンにおいて $0.114 per GB

注意点

  • 1 Item のデータを出力するのに約10分かかりました。5〜10分はかかると思ったほうがよさそうです。
  • 内部的に継続的バックアップ を利用しているそうで、設定が必要です。AWS コンソールで作業を進めていくと、「必要だから設定していい?」と聞かれるのでそれに従えば問題ありません。

料金

Amazon Athena での集計まで含めるともちろんS3データ転送量などがもちろん必要ですが、純粋な Export 機能だと以下で確認できます。

これの AmazonS3へのデータのエクスポート です。私が確認する限り、オンデマンドモードも、Provisioned Capacity も、エクスポート機能に関しては同じ料金でした。東京リージョンで $0.114 per GB です。

pricing.png

やってみる

公式ブログになぞってまずはやってみます。

  1. DynamoDBテーブルとエクスポート用のS3バケットを用意する
  2. AWS コンソールからエクスポートする
  3. エクスポートしたデータを Amazon Athena でクエリする

1. DynamoDBテーブルとエクスポート用のS3バケットを用意する

AWS CDK で構築します。サンプルとして、挨拶文のテンプレートを保存するようなテーブルを用意しました。

greeting-service-stack.ts

import * as cdk from '@aws-cdk/core';
import { Stack } from '@aws-cdk/core';
import * as dynamo from '@aws-cdk/aws-dynamodb';
import { AttributeType } from '@aws-cdk/aws-dynamodb';
import * as s3 from '@aws-cdk/aws-s3';
import { GlobalProps } from './global-props';

export async function greetingServiceApplicationStack(
    scope: cdk.Construct,
    id: string,
    global: GlobalProps,
): Promise<Stack> {
    const stack = new cdk.Stack(scope, id, {
        stackName: global.getStackName(id),
    });

    /**
     * S3 for Dynamodb Export
     */
    new s3.Bucket(stack, 'export', {
        bucketName: global.getBucketName(
            'export',
            stack.account,
            global.getDefaultAppRegion(),
        ),
    });

    /**
     * Greeting Template DynamoDB
     */
    new dynamo.Table(stack, 'TemplateTable', {
        tableName: global.getTableName('Template'),
        partitionKey: { name: 'id', type: AttributeType.STRING },
    });

    return stack;
}

デプロイして2件データを追加します。余談ですが DynamoDB の AWS コンソールもこのタイミングで刷新されていました。

dynamo-created.png

2. AWS コンソールからエクスポートする

Streams and exports > Export to S3 と選択します。

to_export_window.png

次に、Export 設定を入れていきます。今回は公式ブログと同じ設定にしてみます。

export_settings.png

  • Destination S3 bucket
    • s3://dev-greeting-service-export-bucket-ap-northeast-1-0000000000/dev-greeting-service-Template-table としました。今回は同一アカウント・同一リージョンですが、バケットポリシーさえ正しく設定されていれば、クロスアカウント・クロスリージョンでエクスポートできるそうです。
  • Export from a specific point in time
    • Current time のままです。
    • 一番昔を35日前として、どの時点のデータをエクスポートするか指定できます。
    • Export 機能は内部的に継続的バックアップ に依存しているため、このようにエクスポートするポイントを指定できるというわけですね。
  • Encryption key type
    • 暗号化方式を指定できます。

設定が終わったらエクスポートしてみましょう。ジョブが起動して、しばらく待機することになります。

export_started.png

3. エクスポートしたデータを Amazon Athena でクエリする

出力されたデータは以下のようになっていました。日本語も問題なさそうです。

dev-greeting-service-Template-table/AWSDynamoDB/01604986918444-1c619ce8/data/dmazfva2dq6l7jnizdydcivnsi.json.gz

{"Item":{"id":{"S":"a41a21e6-7229-40e8-ae54-60a0ec6d2b5b"},"message":{"S":"新しいテンプレート"},"usedTimes":{"N":"99"}}}
{"Item":{"id":{"S":"a41a21e6-7229-40e8-ae54-60a0ec6d2b5c"},"message":{"S":"テンプレートだよ!"},"usedTimes":{"N":"12"}}}

Amazon Athena でクエリ発行してみます。そのために、Glue Table を AWS CDK で作りましょう。Amazon Athena は、 S3 + Glue Table に対して Presto クエリを発行できるサービスです。いまS3に対象オブジェクトがある状態ですので、クエリを発行するためには追加で Glue Table を用意してあげればOKです。Glue Table の作成は、Amazon Athena で CREATE TABLE を実行することでも実現できます。今回は AWS CDK で Glue Table を作成しました。

greeting-service-stack.ts

import * as cdk from '@aws-cdk/core';
import { Stack } from '@aws-cdk/core';
import * as dynamo from '@aws-cdk/aws-dynamodb';
import { AttributeType } from '@aws-cdk/aws-dynamodb';
import * as s3 from '@aws-cdk/aws-s3';
import * as glue from '@aws-cdk/aws-glue';
import { GlobalProps } from './global-props';

export async function greetingServiceApplicationStack(
    scope: cdk.Construct,
    id: string,
    global: GlobalProps,
): Promise<Stack> {
    const stack = new cdk.Stack(scope, id, {
        stackName: global.getStackName(id),
    });

    /**
     * S3 for Dynamodb Export
     */
    const exportBucket = new s3.Bucket(stack, 'export', {
        bucketName: global.getBucketName(
            'export',
            stack.account,
            global.getDefaultAppRegion(),
        ),
    });

    /**
     * Greeting Template DynamoDB
     */
    new dynamo.Table(stack, 'TemplateTable', {
        tableName: global.getTableName('Template'),
        partitionKey: { name: 'id', type: AttributeType.STRING },
    });

    const db = new glue.Database(stack, 'GreetingDatabase', {
        databaseName: global.getGlueDatabaseName('greeting'),
    });
    new glue.Table(stack, 'TemplateGlueTable', {
        database: db,
        tableName: 'template',
        bucket: exportBucket,
        s3Prefix:
            'dev-greeting-service-Template-table/AWSDynamoDB/01604986918444-1c619ce8/data/',
        dataFormat: glue.DataFormat.JSON,
        columns: [
            {
                name: 'Item',
                type: glue.Schema.struct([
                    {
                        name: 'id',
                        type: glue.Schema.struct([
                            {
                                name: 'S',
                                type: glue.Schema.STRING,
                            },
                        ]),
                    },
                    {
                        name: 'message',
                        type: glue.Schema.struct([
                            {
                                name: 'S',
                                type: glue.Schema.STRING,
                            },
                        ]),
                    },
                    {
                        name: 'usedTimes',
                        type: glue.Schema.struct([
                            {
                                name: 'N',
                                type: glue.Schema.INTEGER,
                            },
                        ]),
                    },
                ]),
            },
        ],
    });

    return stack;
}

glue.Databaseglue.Table を追加した AWS CDK のコードです。出力されたJSONファイルは階層構造になっているため glue.Schema.struct() を使って解決しています。これをデプロイすると Amazon Athena でクエリを発行できるようになります。早速試しましょう。

athena_list.png

無事クエリできました。数値型は集計できるでしょうか。usedtimesを合計してみましょう。

athena_sum.png

合計値もバッチリです。DynamoDB が苦手な集計作業も、このステップ数で実現できるのは嬉しいですね。

まとめ

DynamoDB 標準でS3へのエクスポート機能が実装されたことで特に分析用途での出力作業が楽になりました。もう更新されないはずのテーブルデータをS3へ出力する作業が後回しになっていて、消すに消せず残ったまま…というシーンも多かったのではないでしょうか。あるいは、絶賛稼働中の大量データがある IoT バックエンドテーブルを分析したいが、キャパシティに影響がありそうで手が出せないでいる…という状況もわかります。AWSコンソール上で設定さえすれば読み込みキャパシティに影響を与えることなく出力できるため、運用の観点でも安心感がありますね。ぜひ活用していきましょう。

AWS CDK ソースコード

blog/dynamodb-export: cm-wada-yusuke/aws-serverless-monorepo-starter