TerraformでDynamoDBのテーブルおよび項目作成を動的に行ってみた

TerraformでDynamoDBのテーブルおよび項目作成を動的に行ってみた

Clock Icon2024.08.23

はじめに

コンサルティング部の神野です。
DynamoDBのテーブルをIaCで作成する際に、別管理のテーブル定義ファイルから動的に作成したくなる時があるかと思います。

テーブル項目もマスタデータなどは初回作成時に合わせて投入しておきたいですが、1項目ずつ定義していくのは手間なのでファイルから一括で投入したくなる時などあると思います。

そこで今回は、Terraformを使ってCSVファイルからテーブル定義と項目を読み込み、DynamoDBのテーブル作成とデータ投入を一括で行ってみました。

ディレクトリ構成

下記ディレクトリ構成で進めていきます。
main.tfにリソースの定義を記載し、table_definitions.csvにテーブルの属性定義一覧を、dataフォルダ配下にテーブルに投入するデータを用意します。具体的な取り扱いは後で詳細に説明します。

ディレクトリ構成
.
├── data # データ投入用CSV
│   ├── Orders.csv
│   ├── Products.csv
│   └── Users.csv
├── main.tf # DynamoDBリソース作成定義
└── table_definitions.csv # テーブルの属性定義一覧

テーブル作成

前提

  • テーブル定義をCSVで管理
  • テーブルの属性型はString(S)かNumber(N)のみのシンプルな構成

準備

テーブル定義をCSVで管理し、カラムは下記の通りです。
ファイル名はtable_definitions.csvとしてプロジェクトのルートディレクトリに格納します。

カラム

論理名 物理名 必須か任意か 備考
テーブル名 table_name 必須 Users DynamoDBテーブルの名前
属性名 attribue_name 必須 UserId テーブルの属性(カラム)名
属性種別 attribute_type 必須 S 属性の型。S(String)かN(Number)
キー種別 key_type 任意 HASH HASH(パーティションキー)またはRANGE(ソートキー)

テーブル定義一覧

table_definitions.csv
table_name,attribute_name,attribute_type,key_type
Users,UserId,S,HASH
Users,Name,S,
Users,Age,N,
Products,ProductId,S,HASH
Products,ProductName,S,
Products,Price,N,
Products,StockQuantity,N,
Products,Category,S,
Products,Description,S,
Orders,OrderId,S,HASH
Orders,UserId,S,RANGE
Orders,OrderDate,S,
Orders,TotalAmount,N,
Orders,Status,S,

補足

CSVフォーマットを採用した理由としては下記です。

  • Excelなどで管理されているテーブル定義との互換性が高くCSVをコピペ&置換で作成可能

  • Excelのマクロ出力との相性が良い

今回はあくまで一例で、ファイル形式は組織のテーブル定義管理方法に応じて適宜変更することをお勧めします。

コード

このセクションでは、DynamoDBのテーブルを動的に作成していきます。

  1. CSVファイルからテーブル定義を読み込み、使いやすいデータ構造に変換
  2. 変換したデータ構造を使用して、DynamoDBテーブルを動的に作成

主なポイントは以下の通りです。

  • CSVファイルの内容をローカル変数 tables として構造化
  • 変数tablesの各要素に対してループを実行し、DynamoDBテーブルを作成
  • テーブルの必須項目を設定し、ソートキーが定義されている場合は動的に設定
  • キー種別が HASH (パーティションキー)または RANGE (ソートキー)の属性のみを attribute として設定
main.tf
locals {
  #1. テーブル定義をMap構造に変換
  table_definitions = csvdecode(file("${path.module}/table_definitions.csv"))
  tables = { for table in local.table_definitions : table.table_name => table... }
}
# テーブル作成ブロック
resource "aws_dynamodb_table" "tables" {
  for_each = local.tables 
  name         = each.key
  billing_mode = "PAY_PER_REQUEST"
  #2. HASH(パーティション)キーとRANGE(ソート)キーの設定
  hash_key     = [for attr in each.value : attr.attribute_name if attr.key_type == "HASH"][0]
  range_key    = try([for attr in each.value : attr.attribute_name if attr.key_type == "RANGE"][0], null)

  #3. 属性値の設定
  dynamic "attribute" {
    for_each = [for attr in each.value : attr if attr.key_type == "HASH" || attr.key_type == "RANGE"]
    content {
      name = attribute.value.attribute_name
      type = attribute.value.attribute_type
    }
  }

}

より詳細な説明は3ステップで解説していきます。

  1. テーブル定義をMap変数に変換
  2. パーティションキーとソートキーの設定
  3. 属性値の設定

#1. テーブル定義をMap変数に変換

テーブル定義をCSVから読み込み、使いやすい形式に変換します。

table_definitions = csvdecode(file("${path.module}/table_definitions.csv"))
tables = { for table in local.table_definitions : table.table_name => table... }
  1. csvdecode(file("${path.module}/table_definitions.csv"))
    • file関数: CSVファイルの内容を文字列として読み込みます。
    • csvdecode関数: 読み込んだCSV文字列をリストに変換します。
  2. { for table in local.table_definitions : table.table_name => table... }
    • for式を使用してMap構造を生成しています。
    • table in local.table_definitions: CSVから読み込んだ各行を反復します。
    • table.table_name =>: 各テーブル名をMapのキーとして使用します。
    • table...: スプレッド演算子(...)を使用して、テーブルの全属性をMapの値として設定します。

この結果、tables変数は以下のような構造になります。

  • キー: テーブル名
  • 値: そのテーブルの全属性を含むリスト
変数tablesのイメージ
変数tablesのイメージ
{
  "Users" = [
    {table_name = "Users", attribute_name = "UserId", attribute_type = "S", key_type = "HASH"},
    {table_name = "Users", attribute_name = "Name", attribute_type = "S", key_type = ""},
    {table_name = "Users", attribute_name = "Age", attribute_type = "N", key_type = ""}
  ],
  "Products" = [
    {table_name = "Products", attribute_name = "ProductId", attribute_type = "S", key_type = "HASH"},
    // ... Products テーブルの他の属性 ...
  ],
  // ... 他のテーブル ...
}

#2. パーティションキーとソートキーの設定

各テーブルのパーティションキーとソートキー(存在する場合)を設定します。

  1. パーティションキーの設定
[for attr in each.value : attr.attribute_name if attr.key_type == "HASH"][0]
  • each.value内の各属性を反復し、key_typeHASH(パーティションキー)の属性名を抽出します。
  1. ソートキーの設定
try([for attr in each.value : attr.attribute_name if attr.key_type == "RANGE"][0], null)
  • each.value内の各属性を反復し、key_typeRANGE(ソートキー)の属性名を抽出します。
  • ソートキーが存在しない場合、空のリストから要素を取得しようとしてエラーが発生するため、try関数を使用しています。存在しない場合はnullを設定します。

#3. 属性値の設定

テーブルの属性(attribute)を動的に設定します。

dynamic "attribute" {
  for_each = [for attr in each.value : attr if attr.key_type == "HASH" || attr.key_type == "RANGE"]
  content {
    name = attribute.value.attribute_name
    type = attribute.value.attribute_type
  }
}

このブロックは、テーブル定義の中からキー種別がHASH(パーティションキー)またはRANGE(ソートキー)の属性のみを抽出し、DynamoDBテーブルのattributeとして設定します。これにより、テーブルのキー属性のみが適切に定義されます。

項目作成

このセクションでは、作成したシンプルなテーブルに対して、CSVファイルから項目を作成していきます。

前提

先ほど作成したテーブルにデータを投入するためシンプルなテーブルおよび属性を扱う前提となります。

  • テーブルの項目もCSVで管理
  • テーブルの属性型はString(S)またはNumber(N)のみ

準備

/data/テーブル名.csvで投入したい項目を用意していきます。
Users、Products,Ordersテーブルに対してそれぞれ項目を投入します。

Usersテーブル

属性定義
論理名 物理名 備考
ユーザーID UserId S HASH(パーティションキー)
名前 Name S
年齢 Age N
投入項目
/data/Users.csv
UserId,Name,Age
USER1,Alice,30
USER2,Bob,25
USER3,Taro,13
USER4,Jiro,43

Productsテーブル

属性定義
論理名 物理名 備考
商品ID ProductId S HASH(パーティションキー)
商品名 ProductName S
価格 Price N
在庫数 StockQuantity N
カテゴリー Category S
説明 Description S
投入項目
data/Products.csv
ProductId,ProductName,Price,StockQuantity,Category,Description
101,Smartphone X,699.99,100,Electronics,Latest model with advanced features
102,Laptop Pro,1299.99,50,Electronics,High-performance laptop for professionals
103,Wireless Earbuds,129.99,200,Electronics,True wireless earbuds with noise cancellation
104,Smart Watch,249.99,75,Electronics,Fitness tracker and smartwatch combo
105,Coffee Maker,89.99,150,Home Appliances,Programmable coffee maker with thermal carafe
106,Blender,59.99,100,Home Appliances,Powerful blender for smoothies and more
107,Yoga Mat,29.99,300,Sports & Outdoors,Non-slip yoga mat for all types of yoga
108,Running Shoes,119.99,80,Sports & Outdoors,Lightweight running shoes for long-distance
109,Backpack,49.99,200,Travel & Luggage,Durable backpack with multiple compartments
110,Travel Pillow,19.99,250,Travel & Luggage,Memory foam travel pillow for comfort

Ordersテーブル

属性定義
論理名 物理名 備考
注文ID OrderId S HASH(パーティションキー)
ユーザーID UserId S RANGE(ソートキー)
注文日 OrderDate S
注文金額 TotalAmount N
ステータス Status S
投入項目
data/Orders.csv
OrderId,UserId,OrderDate,TotalAmount,Status
ORD001,USER1,2023-05-01,99.99,Completed
ORD002,USER2,2023-05-02,149.99,Shipped
ORD003,USER1,2023-05-03,79.99,Processing
ORD004,USER3,2023-05-04,199.99,Completed
ORD005,USER2,2023-05-05,59.99,Processing
ORD006,USER4,2023-05-06,129.99,Shipped
ORD007,USER1,2023-05-07,89.99,Processing
ORD008,USER3,2023-05-08,169.99,Completed
ORD009,USER4,2023-05-09,109.99,Shipped
ORD010,USER2,2023-05-10,79.99,Processing

コード

テーブル作成時に使用したmain.tfにCSVファイルからデータを読み込み、DynamoDBテーブルに項目を挿入する処理を追記します。

main.tf
locals {
  table_definitions = csvdecode(file("${path.module}/table_definitions.csv"))
  tables = { for table in local.table_definitions : table.table_name => table... }

+  #1. CSVデータの読み込みと構造化
+  data_files = fileset("${path.module}/data", "*.csv")
+  table_data = {
+    for file in local.data_files :
+    trimsuffix(basename(file), ".csv") => +csvdecode(file("${path.module}/data/${file}"))
+  }
}

# テーブル作成ブロック
resource "aws_dynamodb_table" "tables" {
  # 省略
}

# データ作成ブロック
+resource "aws_dynamodb_table_item" "items" {
+  #2. テーブルデータの1行単位への変換
+  for_each = merge([
+    for table_name, items in local.table_data : {
+      for item in items : (
+        try(
+          [for attr in local.tables[table_name] : attr.attribute_name if  attr.key_type == "RANGE"][0],
+          null
+        ) != null
+        ? "${table_name}:${item[[for attr in local.tables[table_name] : attr.attribute_name if attr.key_type == "HASH"][0]]}:${item[[for attr in +local.tables[table_name] : attr.attribute_name if attr.key_type == +v"RANGE"][0]]}"
+        : "${table_name}:${item[[for attr in local.tables[table_name] : attr.attribute_name if attr.key_type == "HASH"][0]]}"
+      ) => {
+        table_name = table_name
+        item       = item
+      }
+    }
+  ]...)
+
+  table_name = each.value.table_name
+  #3. パーティションキーとソートキーの設定
+  hash_key   = [for attr in local.tables[each.value.table_name] : +attr.attribute_name if attr.key_type == "HASH"][0]
+  range_key  = try([for attr in local.tables[each.value.table_name] : +attr.attribute_name if attr.key_type == "RANGE"][0], null)
+
+  #4. 項目値の設定
+  item = jsonencode({
+    for k, v in each.value.item :
+    k => {
+      [for attr in local.tables[each.value.table_name] : attr.attribute_type if attr.attribute_name == k][0] = tostring(v)
+    }
+  })
+
+  depends_on = [aws_dynamodb_table.tables]
+}

より詳細な説明は4ステップで解説していきます。

  1. CSVデータの読み込みと構造化
  2. テーブルデータの1行単位への変換
  3. パーティションキーとソートキーの設定
  4. 項目値の設定

#1. CSVデータの読み込みと構造化

data_files = fileset("${path.module}/data", "*.csv")
table_data = {
  for file in local.data_files :
  trimsuffix(basename(file), ".csv") => csvdecode(file("${path.module}/data/${file}"))
}
  1. fileset("${path.module}/data", "*.csv"):

    • dataフォルダ内の全てのCSVファイルのパスを取得します。
  2. for file in local.data_files : ...:

    • 各CSVファイルに対してループを実行します。
  3. trimsuffix(basename(file), ".csv"):

    • ファイル名から.csv拡張子を除去し、テーブル名として使用します。
  4. csvdecode(file("${path.module}/data/${file}")):

    • CSVファイルの内容を読み込み、デコードしてリスト形式のデータに変換します。

この処理により、各テーブルの項目がMap形式でtable_data変数に格納されます。

変数table_dataのイメージ
table_data = {
  "users" = [
    {
      "UserId" = "USER1"
      "Name" = "Alice"
      "Age" = "30"
    },
    {
      "UserId" = "USER2"
      "Name" = "Bob"
      "Age" = "25"
    },
    // ... 他のユーザー
  ],
  "products" = [
    {
      "ProductId" = "101"
      "ProductName" = "Smartphone X"
      "Price" = "699.99"
      "StockQuantity" = "100"
      "Category" = "Electronics"
      "Description" = "Latest model with advanced features"
    },
    {
      "ProductId" = "102"
      "ProductName" = "Laptop Pro"
      "Price" = "1299.99"
      "StockQuantity" = "50"
      "Category" = "Electronics"
      "Description" = "High-performance laptop for professionals"
    },
    // ... 他の製品
  ],
  "orders" = [
    {
      "OrderId" = "ORD001"
      "UserId" = "USER1"
      "OrderDate" = "2023-05-01"
      "TotalAmount" = "99.99"
      "Status" = "Completed"
    },
    {
      "OrderId" = "ORD002"
      "UserId" = "USER2"
      "OrderDate" = "2023-05-02"
      "TotalAmount" = "149.99"
      "Status" = "Shipped"
    },
    // ... 他の注文
  ]
}

#2. テーブルデータの1行単位への変換

for_each = merge([
  for table_name, items in local.table_data : {
    for idx, item in items : "${table_name}-${idx}" => {
      table_name = table_name
      item       = item
    }
  }
]...)

この部分は複数の処理を組み合わせて、テーブル項目を1行単位に変換しています:

  1. 外側のループ: for table_name, items in local.table_data : ...
    • 各テーブルとそのデータに対してループを実行します。
  2. 内側のループ: for item in items : ...
    • 各テーブル内の個々の項目(行)に対してループを実行します。
  3. キーの生成:
    • パーティションキーとソートキー(存在する場合)を使用してユニークなキーを生成します。
    • ソートキーが存在する場合: "${table_name}:${パーティションキーの値}:${ソートキーの値}"
    • ソートキーが存在しない場合: "${table_name}:${パーティションキーの値}"
  4. 値の構造: { table_name = table_name, item = item }
    • テーブル名と項目データを含む構造を作成します。
  5. merge([...]):
    • 全てのテーブルと項目から生成されたマップを1つの大きなマップに統合します。

この処理により、各テーブルの各項目が個別のエントリとして扱えるようになり、 DynamoDBの項目として挿入する準備が整います。

作成されるデータ構造のイメージ
{
  "Users:USER1" = {
    table_name = "Users"
    item = {
      "UserId" = "USER1"
      "Name" = "Alice"
      "Age" = "30"
    }
  },
  "Users:USER2" = {
    table_name = "Users"
    item = {
      "UserId" = "USER2"
      "Name" = "Bob"
      "Age" = "25"
    }
  },
  "Products:101" = {
    table_name = "Products"
    item = {
      "ProductId" = "101"
      "ProductName" = "Smartphone X"
      "Price" = "699.99"
      "StockQuantity" = "100"
      "Category" = "Electronics"
      "Description" = "Latest model with advanced features"
    }
  },
  "Orders:ORD001:USER1" = {
    table_name = "Orders"
    item = {
      "OrderId" = "ORD001"
      "UserId" = "USER1"
      "OrderDate" = "2023-05-01"
      "TotalAmount" = "99.99"
      "Status" = "Completed"
    }
  },
  // ... 他の項目 ...
}

#3. パーティションキーとソートキーの設定

hash_key   = [for attr in local.tables[each.value.table_name] : attr.attribute_name if attr.key_type == "HASH"][0]
range_key  = try([for attr in local.tables[each.value.table_name] : attr.attribute_name if attr.key_type == "RANGE"][0], null)

テーブル作成時と同様にテーブル定義からパーティションキーとソートキーの属性を探索して設定します。

#4. 項目値の設定

item = jsonencode({
  for k, v in each.value.item :
  k => {
    [for attr in local.tables[each.value.table_name] : attr.attribute_type if attr.attribute_name == k][0] = tostring(v)
  }
})

この部分では、DynamoDBの項目を適切な形式で設定しています。

  1. jsonencode(...): 生成された構造をJSON形式にエンコードします。
  2. 外側のループ for k, v in each.value.item ::
    • 現在の項目(行)の各属性(列)に対してループを実行します。
  3. 内側のループ [for attr in local.tables[each.value.table_name] :attr.attribute_type if attr.attribute_name == k]:
    • テーブル定義から、現在の属性に一致する属性型を探します。
    • 属性名が一致した場合、その属性の型(S: String, N: Number)を取得します。
  4. tostring(v):
    • 値を文字列に変換します。DynamoDBは属性値を文字列として受け取ります。

この処理により、各項目のデータが以下のような形式に変換されます。

{
  "UserId": {"S": "USER1"},
  "Name": {"S": "Alice"},
  "Age": {"N": "30"}
}

構築

コードが書けたところで問題なく作成できるかplanコマンドを実行して確認します。

planコマンド

terraform plan

plan結果

出力内容(長いため折りたたんでいます)
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_dynamodb_table.tables["Orders"] will be created
  + resource "aws_dynamodb_table" "tables" {
      + arn              = (known after apply)
      + billing_mode     = "PAY_PER_REQUEST"
      + hash_key         = "OrderId"
      + id               = (known after apply)
      + name             = "Orders"
      + range_key        = "UserId"
      + read_capacity    = (known after apply)
      + stream_arn       = (known after apply)
      + stream_label     = (known after apply)
      + stream_view_type = (known after apply)
      + tags_all         = (known after apply)
      + write_capacity   = (known after apply)

      + attribute {
          + name = "OrderId"
          + type = "S"
        }
      + attribute {
          + name = "UserId"
          + type = "S"
        }

      + point_in_time_recovery (known after apply)

      + server_side_encryption (known after apply)

      + ttl (known after apply)
    }

  # aws_dynamodb_table.tables["Products"] will be created
  + resource "aws_dynamodb_table" "tables" {
      + arn              = (known after apply)
      + billing_mode     = "PAY_PER_REQUEST"
      + hash_key         = "ProductId"
      + id               = (known after apply)
      + name             = "Products"
      + read_capacity    = (known after apply)
      + stream_arn       = (known after apply)
      + stream_label     = (known after apply)
      + stream_view_type = (known after apply)
      + tags_all         = (known after apply)
      + write_capacity   = (known after apply)

      + attribute {
          + name = "ProductId"
          + type = "S"
        }

      + point_in_time_recovery (known after apply)

      + server_side_encryption (known after apply)

      + ttl (known after apply)
    }

  # aws_dynamodb_table.tables["Users"] will be created
  + resource "aws_dynamodb_table" "tables" {
      + arn              = (known after apply)
      + billing_mode     = "PAY_PER_REQUEST"
      + hash_key         = "UserId"
      + id               = (known after apply)
      + name             = "Users"
      + read_capacity    = (known after apply)
      + stream_arn       = (known after apply)
      + stream_label     = (known after apply)
      + stream_view_type = (known after apply)
      + tags_all         = (known after apply)
      + write_capacity   = (known after apply)

      + attribute {
          + name = "UserId"
          + type = "S"
        }

      + point_in_time_recovery (known after apply)

      + server_side_encryption (known after apply)

      + ttl (known after apply)
    }

  # aws_dynamodb_table_item.items["Orders:ORD001:USER1"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "OrderId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + OrderDate   = {
                  + S = "2023-05-01"
                }
              + OrderId     = {
                  + S = "ORD001"
                }
              + Status      = {
                  + S = "Completed"
                }
              + TotalAmount = {
                  + N = "99.99"
                }
              + UserId      = {
                  + S = "USER1"
                }
            }
        )
      + range_key  = "UserId"
      + table_name = "Orders"
    }

  # aws_dynamodb_table_item.items["Orders:ORD002:USER2"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "OrderId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + OrderDate   = {
                  + S = "2023-05-02"
                }
              + OrderId     = {
                  + S = "ORD002"
                }
              + Status      = {
                  + S = "Shipped"
                }
              + TotalAmount = {
                  + N = "149.99"
                }
              + UserId      = {
                  + S = "USER2"
                }
            }
        )
      + range_key  = "UserId"
      + table_name = "Orders"
    }

  # aws_dynamodb_table_item.items["Orders:ORD003:USER1"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "OrderId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + OrderDate   = {
                  + S = "2023-05-03"
                }
              + OrderId     = {
                  + S = "ORD003"
                }
              + Status      = {
                  + S = "Processing"
                }
              + TotalAmount = {
                  + N = "79.99"
                }
              + UserId      = {
                  + S = "USER1"
                }
            }
        )
      + range_key  = "UserId"
      + table_name = "Orders"
    }

  # aws_dynamodb_table_item.items["Orders:ORD004:USER3"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "OrderId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + OrderDate   = {
                  + S = "2023-05-04"
                }
              + OrderId     = {
                  + S = "ORD004"
                }
              + Status      = {
                  + S = "Completed"
                }
              + TotalAmount = {
                  + N = "199.99"
                }
              + UserId      = {
                  + S = "USER3"
                }
            }
        )
      + range_key  = "UserId"
      + table_name = "Orders"
    }

  # aws_dynamodb_table_item.items["Orders:ORD005:USER2"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "OrderId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + OrderDate   = {
                  + S = "2023-05-05"
                }
              + OrderId     = {
                  + S = "ORD005"
                }
              + Status      = {
                  + S = "Processing"
                }
              + TotalAmount = {
                  + N = "59.99"
                }
              + UserId      = {
                  + S = "USER2"
                }
            }
        )
      + range_key  = "UserId"
      + table_name = "Orders"
    }

  # aws_dynamodb_table_item.items["Orders:ORD006:USER4"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "OrderId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + OrderDate   = {
                  + S = "2023-05-06"
                }
              + OrderId     = {
                  + S = "ORD006"
                }
              + Status      = {
                  + S = "Shipped"
                }
              + TotalAmount = {
                  + N = "129.99"
                }
              + UserId      = {
                  + S = "USER4"
                }
            }
        )
      + range_key  = "UserId"
      + table_name = "Orders"
    }

  # aws_dynamodb_table_item.items["Orders:ORD007:USER1"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "OrderId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + OrderDate   = {
                  + S = "2023-05-07"
                }
              + OrderId     = {
                  + S = "ORD007"
                }
              + Status      = {
                  + S = "Processing"
                }
              + TotalAmount = {
                  + N = "89.99"
                }
              + UserId      = {
                  + S = "USER1"
                }
            }
        )
      + range_key  = "UserId"
      + table_name = "Orders"
    }

  # aws_dynamodb_table_item.items["Orders:ORD008:USER3"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "OrderId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + OrderDate   = {
                  + S = "2023-05-08"
                }
              + OrderId     = {
                  + S = "ORD008"
                }
              + Status      = {
                  + S = "Completed"
                }
              + TotalAmount = {
                  + N = "169.99"
                }
              + UserId      = {
                  + S = "USER3"
                }
            }
        )
      + range_key  = "UserId"
      + table_name = "Orders"
    }

  # aws_dynamodb_table_item.items["Orders:ORD009:USER4"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "OrderId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + OrderDate   = {
                  + S = "2023-05-09"
                }
              + OrderId     = {
                  + S = "ORD009"
                }
              + Status      = {
                  + S = "Shipped"
                }
              + TotalAmount = {
                  + N = "109.99"
                }
              + UserId      = {
                  + S = "USER4"
                }
            }
        )
      + range_key  = "UserId"
      + table_name = "Orders"
    }

  # aws_dynamodb_table_item.items["Orders:ORD010:USER2"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "OrderId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + OrderDate   = {
                  + S = "2023-05-10"
                }
              + OrderId     = {
                  + S = "ORD010"
                }
              + Status      = {
                  + S = "Processing"
                }
              + TotalAmount = {
                  + N = "79.99"
                }
              + UserId      = {
                  + S = "USER2"
                }
            }
        )
      + range_key  = "UserId"
      + table_name = "Orders"
    }

  # aws_dynamodb_table_item.items["Products:101"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "ProductId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Category      = {
                  + S = "Electronics"
                }
              + Description   = {
                  + S = "Latest model with advanced features"
                }
              + Price         = {
                  + N = "699.99"
                }
              + ProductId     = {
                  + S = "101"
                }
              + ProductName   = {
                  + S = "Smartphone X"
                }
              + StockQuantity = {
                  + N = "100"
                }
            }
        )
      + table_name = "Products"
    }

  # aws_dynamodb_table_item.items["Products:102"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "ProductId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Category      = {
                  + S = "Electronics"
                }
              + Description   = {
                  + S = "High-performance laptop for professionals"
                }
              + Price         = {
                  + N = "1299.99"
                }
              + ProductId     = {
                  + S = "102"
                }
              + ProductName   = {
                  + S = "Laptop Pro"
                }
              + StockQuantity = {
                  + N = "50"
                }
            }
        )
      + table_name = "Products"
    }

  # aws_dynamodb_table_item.items["Products:103"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "ProductId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Category      = {
                  + S = "Electronics"
                }
              + Description   = {
                  + S = "True wireless earbuds with noise cancellation"
                }
              + Price         = {
                  + N = "129.99"
                }
              + ProductId     = {
                  + S = "103"
                }
              + ProductName   = {
                  + S = "Wireless Earbuds"
                }
              + StockQuantity = {
                  + N = "200"
                }
            }
        )
      + table_name = "Products"
    }

  # aws_dynamodb_table_item.items["Products:104"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "ProductId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Category      = {
                  + S = "Electronics"
                }
              + Description   = {
                  + S = "Fitness tracker and smartwatch combo"
                }
              + Price         = {
                  + N = "249.99"
                }
              + ProductId     = {
                  + S = "104"
                }
              + ProductName   = {
                  + S = "Smart Watch"
                }
              + StockQuantity = {
                  + N = "75"
                }
            }
        )
      + table_name = "Products"
    }

  # aws_dynamodb_table_item.items["Products:105"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "ProductId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Category      = {
                  + S = "Home Appliances"
                }
              + Description   = {
                  + S = "Programmable coffee maker with thermal carafe"
                }
              + Price         = {
                  + N = "89.99"
                }
              + ProductId     = {
                  + S = "105"
                }
              + ProductName   = {
                  + S = "Coffee Maker"
                }
              + StockQuantity = {
                  + N = "150"
                }
            }
        )
      + table_name = "Products"
    }

  # aws_dynamodb_table_item.items["Products:106"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "ProductId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Category      = {
                  + S = "Home Appliances"
                }
              + Description   = {
                  + S = "Powerful blender for smoothies and more"
                }
              + Price         = {
                  + N = "59.99"
                }
              + ProductId     = {
                  + S = "106"
                }
              + ProductName   = {
                  + S = "Blender"
                }
              + StockQuantity = {
                  + N = "100"
                }
            }
        )
      + table_name = "Products"
    }

  # aws_dynamodb_table_item.items["Products:107"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "ProductId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Category      = {
                  + S = "Sports & Outdoors"
                }
              + Description   = {
                  + S = "Non-slip yoga mat for all types of yoga"
                }
              + Price         = {
                  + N = "29.99"
                }
              + ProductId     = {
                  + S = "107"
                }
              + ProductName   = {
                  + S = "Yoga Mat"
                }
              + StockQuantity = {
                  + N = "300"
                }
            }
        )
      + table_name = "Products"
    }

  # aws_dynamodb_table_item.items["Products:108"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "ProductId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Category      = {
                  + S = "Sports & Outdoors"
                }
              + Description   = {
                  + S = "Lightweight running shoes for long-distance"
                }
              + Price         = {
                  + N = "119.99"
                }
              + ProductId     = {
                  + S = "108"
                }
              + ProductName   = {
                  + S = "Running Shoes"
                }
              + StockQuantity = {
                  + N = "80"
                }
            }
        )
      + table_name = "Products"
    }

  # aws_dynamodb_table_item.items["Products:109"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "ProductId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Category      = {
                  + S = "Travel & Luggage"
                }
              + Description   = {
                  + S = "Durable backpack with multiple compartments"
                }
              + Price         = {
                  + N = "49.99"
                }
              + ProductId     = {
                  + S = "109"
                }
              + ProductName   = {
                  + S = "Backpack"
                }
              + StockQuantity = {
                  + N = "200"
                }
            }
        )
      + table_name = "Products"
    }

  # aws_dynamodb_table_item.items["Products:110"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "ProductId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Category      = {
                  + S = "Travel & Luggage"
                }
              + Description   = {
                  + S = "Memory foam travel pillow for comfort"
                }
              + Price         = {
                  + N = "19.99"
                }
              + ProductId     = {
                  + S = "110"
                }
              + ProductName   = {
                  + S = "Travel Pillow"
                }
              + StockQuantity = {
                  + N = "250"
                }
            }
        )
      + table_name = "Products"
    }

  # aws_dynamodb_table_item.items["Users:USER1"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "UserId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Age    = {
                  + N = "30"
                }
              + Name   = {
                  + S = "Alice"
                }
              + UserId = {
                  + S = "USER1"
                }
            }
        )
      + table_name = "Users"
    }

  # aws_dynamodb_table_item.items["Users:USER2"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "UserId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Age    = {
                  + N = "25"
                }
              + Name   = {
                  + S = "Bob"
                }
              + UserId = {
                  + S = "USER2"
                }
            }
        )
      + table_name = "Users"
    }

  # aws_dynamodb_table_item.items["Users:USER3"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "UserId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Age    = {
                  + N = "13"
                }
              + Name   = {
                  + S = "Taro"
                }
              + UserId = {
                  + S = "USER3"
                }
            }
        )
      + table_name = "Users"
    }

  # aws_dynamodb_table_item.items["Users:USER4"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "UserId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Age    = {
                  + N = "43"
                }
              + Name   = {
                  + S = "Jiro"
                }
              + UserId = {
                  + S = "USER4"
                }
            }
        )
      + table_name = "Users"
    }

Plan: 27 to add, 0 to change, 0 to destroy.

planコマンド上はテーブルもデータも問題なく作成されそうですね。
作成のコマンドも実行します。

実行コマンド

実行コマンド
terraform apply

実行結果

実行結果
// 諸々と出力されているが省略
aws_dynamodb_table_item.items["Users:USER1"]: Creation complete after 1s [id=Users|UserId|USER1]
aws_dynamodb_table_item.items["Products:101"]: Creation complete after 1s [id=Products|ProductId|101]
aws_dynamodb_table_item.items["Products:108"]: Creation complete after 1s [id=Products|ProductId|108]
aws_dynamodb_table_item.items["Orders:ORD005:USER2"]: Creation complete after 1s [id=Orders|OrderId|ORD005|USER2]

Apply complete! Resources: 27 added, 0 changed, 0 destroyed.

無事作成されましたね!後はコンソール上でもテーブルおよびデータが存在しているか確認してみます。

環境確認

テーブル一覧

スクリーンショット 2024-08-20 23.01.34

Usersテーブル

スクリーンショット 2024-08-20 23.02.43

Productsテーブル

スクリーンショット 2024-08-20 23.02.29

Ordersテーブル

スクリーンショット 2024-08-20 23.02.08

テーブルもデータも全て期待通り作成されていますね!

項目変更時

テーブルのデータをCSVで管理していますが、もし変更・追加・削除した際は適切に変更されるのでしょうか?
data/Users.csvに変更を加えて確認してみます。

変更内容

data/Users.csv
UserId,Name,Age
USER1,Alice,30
USER2,Bob,25
# USER3を削除
-USER3,Taro,13
# 年齢を61へ変更
USER4,Jiro,61
# USER5を追加
+USER5,Sabu,12
  • USER5を追加
  • USER4の年齢を4361に変更
  • USER3を削除

変更を加えた状態で適切に変更されるかplanコマンドを実行して確認してみます。

planコマンド実行

terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  + create
  ~ update in-place
  - destroy

Terraform will perform the following actions:

  # aws_dynamodb_table_item.items["Users:USER3"] will be destroyed
  # (because key ["Users:USER3"] is not in for_each map)
  - resource "aws_dynamodb_table_item" "items" {
      - hash_key   = "UserId" -> null
      - id         = "Users|UserId|USER3" -> null
      - item       = jsonencode(
            {
              - Age    = {
                  - N = "13"
                }
              - Name   = {
                  - S = "Taro"
                }
              - UserId = {
                  - S = "USER3"
                }
            }
        ) -> null
      - table_name = "Users" -> null
    }

  # aws_dynamodb_table_item.items["Users:USER4"] will be updated in-place
  ~ resource "aws_dynamodb_table_item" "items" {
        id         = "Users|UserId|USER4"
      ~ item       = jsonencode(
          ~ {
              ~ Age    = {
                  ~ N = "43" -> "61"
                }
                # (2 unchanged attributes hidden)
            }
        )
        # (2 unchanged attributes hidden)
    }

  # aws_dynamodb_table_item.items["Users:USER5"] will be created
  + resource "aws_dynamodb_table_item" "items" {
      + hash_key   = "UserId"
      + id         = (known after apply)
      + item       = jsonencode(
            {
              + Age    = {
                  + N = "12"
                }
              + Name   = {
                  + S = "Sabu"
                }
              + UserId = {
                  + S = "USER5"
                }
            }
        )
      + table_name = "Users"
    }

Plan: 1 to add, 1 to change, 1 to destroy.

出力結果を見ると問題なさそうですね!
念の為実行もして結果を確認します。

実行結果

terraform apply

aws_dynamodb_table_item.items["Users:USER3"]: Destroying... [id=Users|UserId|USER3]
aws_dynamodb_table_item.items["Users:USER5"]: Creating...
aws_dynamodb_table_item.items["Users:USER4"]: Modifying... [id=Users|UserId|USER4]
aws_dynamodb_table_item.items["Users:USER5"]: Creation complete after 0s [id=Users|UserId|USER5]
aws_dynamodb_table_item.items["Users:USER4"]: Modifications complete after 0s [id=Users|UserId|USER4]
aws_dynamodb_table_item.items["Users:USER3"]: Destruction complete after 0s
Apply complete! Resources: 1 added, 1 changed, 1 destroyed.

環境確認

Ordersテーブル

スクリーンショット 2024-08-23 14.31.33

実行も成功してコンソール上からも変更を確認できました!
変更にも対応できていますね。

全体のソースコード

部分的にコードを解説していたので最後にmain.tf全てを共有します。

main.tf
locals {
  table_definitions = csvdecode(file("${path.module}/table_definitions.csv"))
  tables = { for table in local.table_definitions : table.table_name => table... }

  data_files = fileset("${path.module}/data", "*.csv")
  table_data = {
    for file in local.data_files :
    trimsuffix(basename(file), ".csv") => csvdecode(file("${path.module}/data/${file}"))
  }
}

# テーブル作成ブロック
resource "aws_dynamodb_table" "tables" {
  for_each = local.tables

  name         = each.key
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = [for attr in each.value : attr.attribute_name if attr.key_type == "HASH"][0]
  range_key    = try([for attr in each.value : attr.attribute_name if attr.key_type == "RANGE"][0], null)

  dynamic "attribute" {
    for_each = [for attr in each.value : attr if attr.key_type == "HASH" || attr.key_type == "RANGE"]
    content {
      name = attribute.value.attribute_name
      type = attribute.value.attribute_type
    }
  }
}

# 項目作成ブロック
resource "aws_dynamodb_table_item" "items" {
  for_each = merge([
    for table_name, items in local.table_data : {
      for item in items : (
        try(
          [for attr in local.tables[table_name] : attr.attribute_name if attr.key_type == "RANGE"][0],
          null
        ) != null
        ? "${table_name}:${item[[for attr in local.tables[table_name] : attr.attribute_name if attr.key_type == "HASH"][0]]}:${item[[for attr in local.tables[table_name] : attr.attribute_name if attr.key_type == "RANGE"][0]]}"
        : "${table_name}:${item[[for attr in local.tables[table_name] : attr.attribute_name if attr.key_type == "HASH"][0]]}"
      ) => {
        table_name = table_name
        item       = item
      }
    }
  ]...)

  table_name = each.value.table_name
  hash_key   = [for attr in local.tables[each.value.table_name] : attr.attribute_name if attr.key_type == "HASH"][0]
  range_key  = try([for attr in local.tables[each.value.table_name] : attr.attribute_name if attr.key_type == "RANGE"][0], null)

  item = jsonencode({
    for k, v in each.value.item :
    k => {
      [for attr in local.tables[each.value.table_name] : attr.attribute_type if attr.attribute_name == k][0] = tostring(v)
    }
  })

  depends_on = [aws_dynamodb_table.tables]
}

終わりに

TerraformとCSVを使ってDynamoDBのテーブルとデータを一括で作成してみましたがいかがでしたでしょうか?
実際のテーブル構造は複雑なケースもあるため、考慮する箇所は増えるかと思いますがシンプルなテーブル構造なら本記事のように対応できるのではないかと思います!
少しでもお役に立ちましたら幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.