🚚

Step FunctionsによるDynamoDB増分エクスポート(w/ Terraform)

に公開

本記事は、Japan Digital Design Advent Calendar 2025 の 8 日目の記事になります。

https://adventar.org/calendars/11598


三菱 UFJ フィナンシャル・グループ(以下 MUFG)の戦略子会社である Japan Digital Design(以下 JDD)でフロント/バックエンドのソフトウェアエンジニアをしている渡邉です。

本記事では、DynamoDB の増分エクスポートの実装について共有します。

はじめに

前職では

  • メインのサービスはオンプレ
  • 別のサービスで AWS の Web コンソール上でぽちぽちしてシステム構築

くらいで、IaC も Step Functions も業務での使用経験はありませんでした。

JDD ではクラウドでのシステム構築が基本であり、どこかで触るチャンスないかな👀と探していました。

そんなときに

DynamoDB のデータを DS チームで解析を行うため S3 に配置する

という、ちょうどよい感じのタスクがあったのでこれをもらうことにしました。

EventBridge → Step Functions → DynamoDB INCREMENTAL_EXPORT → S3

という構成を Terraform で実装したので、その流れを書いていきます。

概要

やりたかったことはシンプルに言うと、

  • DynamoDB のデータを
  • 毎日定時に
  • 増分で
  • S3 にエクスポートする

というバッチ処理です。

これを以下の構成で実装しました。

  • EventBridge: cron 表記による定期起動
  • Step Functions: DynamoDB のテーブルを Map で一括処理
  • DynamoDB: INCREMENTAL_EXPORTを使用して増分エクスポート
  • S3: エクスポート先バケット

リランを考慮し、エクスポート対象時間をリクエストで渡すこともできるようにしておき、
指定がなければ JST 基準で「前日 0:00〜当日 0:00」を自動計算して使う、という形にしています。

全体アーキテクチャ

ざっくり全体像はこんな感じです。

  • EventBridge のルールで 1 日 1 回 深夜帯に Step Functions を起動
  • Step Functions の Input に、エクスポート対象のテーブル一覧をexports配列として渡す
  • Mapステートでテーブルごとに処理
  • 各テーブルごとに
    • エクスポートする時間範囲を決める
    • S3 のプレフィックスを組み立てる
    • exportTableToPointInTimeINCREMENTAL_EXPORT)を実行

Step Functions の Terraform 実装例とステートごとの解説

ステートマシン全体の流れ

この一連の処理を、Mapステートを使ってテーブルごとに回しています。

Terraform 上の definition

definition = jsonencode({
  Comment = "DynamoDB Incremental Export to S3"
  StartAt = "ExportTables"
  States = {
    ExportTables = {
      Type      = "Map"
      ItemsPath = "$.exports"
      Iterator = {
        StartAt = "CheckTimeRange"
        States = {
          // 各ステートの詳細は後述
        }
      }
    }
  }
})

$.exportsには、このような JSON を想定しています

{
  "exports": [
    {
      "table_arn": "出力対象テーブルの ARN",
      "s3_bucket": "出力先バケット名",
      "s3_prefix": "出力先のプレフィックス",
      "export_format": "DYNAMODB_JSON"
    }
    // 必要に応じて複数テーブル分並べる
  ]
}

ここから、各テーブルごとに以下のステートを順に実行していきます。

CheckTimeRange

入力にexport_from_timeexport_to_timeが含まれていればそのまま使い、
なければ後続ステートで自動生成するよう分岐します(JSONata による判定)。

CheckTimeRange = {
  Type          = "Choice"
  QueryLanguage = "JSONata"
  Output        = "{% $states.input %}"
  Default       = "PrepareTimeRange"
  Choices = [
    {
      Condition = "{% $states.input.export_from_time and $states.input.export_to_time %}"
      Next      = "AssignExistingTime"
    }
  ]
},

概要で触れたリラン対応のため以下のように分岐させています。

  • エクスポート期間の指定あり --> AssignExistingTime
  • エクスポート期間の指定なし --> PrepareTimeRange

AssignExistingTime

すでにexport_from_time / export_to_timeが渡されている場合、それをそのまま使うステップです。

AssignExistingTime = {
  Type          = "Pass"
  QueryLanguage = "JSONata"
  Output        = "{% $states.input %}"
  Assign = {
    "export_to_time"   = "{% $states.input.export_to_time %}"
    "export_from_time" = "{% $states.input.export_from_time %}"
  }
  Next = "FormatS3Prefix"
},

Passステート+Assignで、入力の時間範囲を変数としてセットしています。
ここでセットした変数は、後続のステートで $export_from_time のように参照できます。

PrepareTimeRange

時間指定がなかった場合、現在の JST 日付をベースに

  • export_from_time --> 前日 0:00(JST)
  • export_to_time --> 当日 0:00(JST)

を自動算出します。

PrepareTimeRange = {
  Type          = "Pass"
  QueryLanguage = "JSONata"
  Assign = {
    "export_to_time"   = "{% $millis() + 9 * 3600000 ~> $fromMillis('[Y0001]-[M01]-[D01]T00:00:00+09:00') %}"
    "export_from_time" = "{% $millis() + 9 * 3600000 - 86400000 ~> $fromMillis('[Y0001]-[M01]-[D01]T00:00:00+09:00') %}"
  }
  Next = "FormatS3Prefix"
},

このようなタイムゾーンが絡むちょっとした日付計算も、JSONata を使えば Lambda を用意することなく Step Functions 内で完結できました!

FormatS3Prefix

日付(yyyyMMdd形式)を含んだ S3 プレフィックスを作成します。

FormatS3Prefix = {
  Type          = "Pass"
  QueryLanguage = "JSONata"
  Output        = "{% $states.input %}"
  Assign = {
    "s3_prefix_with_date" = "{% ($substringBefore($export_from_time, 'T') ~> $replace('-', '')) & '/' & $states.input.s3_prefix %}"
  }
  Next = "ExportTable"
},

例えば

  • export_from_time = "2025-06-02T00:00:00+09:00"
  • s3_prefix = "テーブル名"

の場合、

  • s3_prefix_with_date = "20250602/テーブル名"

という形式になります。

日付ごとにディレクトリが分かれていたほうが、取り込み時にも便利なためこのような構成としました。

こんな感じのちょっとした文字列操作も、JSONata を使えば割とお手軽に実現できました!

ExportTable

最終的に DynamoDB のexportTableToPointInTimeを呼び出して、
指定した期間の増分エクスポートを S3 に保存します。

ExportTable = {
  Type     = "Task"
  Resource = "arn:aws:states:::aws-sdk:dynamodb:exportTableToPointInTime"
  Parameters = {
    "TableArn.$"     = "$.table_arn"
    "S3Bucket.$"     = "$.s3_bucket"
    "S3Prefix.$"     = "$s3_prefix_with_date"
    "ExportFormat.$" = "$.export_format"
    "ExportType"     = "INCREMENTAL_EXPORT"
    "IncrementalExportSpecification" = {
      "ExportFromTime.$" = "$export_from_time"
      "ExportToTime.$"   = "$export_to_time"
      "ExportViewType"   = "NEW_IMAGE"
    }
  }
  End = true
}

Tips

JSONata の式を試行錯誤するとき、公式のオンラインエディタ(Exerciser)がかなり使えました。

https://try.jsonata.org/

ブラウザ上でサクッと式を試せるので、Step Functions にデプロイする前にここで動作確認しておくと捗りました!

まとめ

今回、初めて Step Functions を使用してみたのですが、JSONata 便利だなと思いました。
それ以外にも、Map ステートで並列処理が簡単にできたり、コンソールで実行状況が可視化できたりと、とても使い勝手が良かったです。

「クラウドインフラを業務でもう少し触ってみたいな」と思っていた自分にとって、ちょうどいいサイズ感のタスクでとても勉強になりました。(AWS 強いメンバーにコードレビューもしていただけました!)

JDD では手を挙げれば色々なことに挑戦させてもらえるので、今後もチャレンジしたいと思います。


以上、渡邉でした。

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


Japan Digital Design 株式会社では、一緒に働いてくださる仲間を募集中です。カジュアル面談も実施しておりますので下記リンク先からお気軽にお問合せください。

https://japan-d2.com/careers

この記事に関するお問い合わせはこちら

https://japan-d2.com/contact

Technology & Development Div.

Ryota Watanabe

Discussion