【Terraform】NoSQL workbenchで作成したDynamoDB の定義をもとにリソースを作成する

# TL;DR

* NoSQL workbenchは作成したdata modelをexportできる
* exportした定義をもとにTerraformでリソースを作成したい
* 既存の仕組みではできなそうなので、自分で作りました

https://github.com/willco21/terrafrom-dynamodb-from-data-model

## NoSQL workbenchとは

>NoSQL Workbench は、DynamoDB テーブルの設計、作成、クエリ、管理に役立つデータモデリング、データ可視化、クエリ開発といった特徴を提供する視覚的開発ツールです。

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/workbench.html

https://aws.amazon.com/jp/dynamodb/nosql-workbench/?nc1=h_ls

要するに、DynamoDB用のDBツール見たいな感じです。RDBに比べてなじみが薄いDynamoDBの設計・実装を楽にしてくれます

NoSQL workbenchというぐらいなので、Amazon Keyspacesにも対応していたりします

AWSのCredentialを設定すれば、デプロイまでできてしまいます

## Terraform でデプロイしたい

上述の通り、NoSQL Workbench自体でデプロイもできるのですが、かかわったプロジェクトではNoSQL workbenchで設計したdata modelをリポジトリにコミットして、開発者間で変更共有していました。そして、クラウドにはTerraformでIaCをしており、そこにまとめて管理しておきたいという要件がありました。

単純にTerraformでやる場合だと、data modelの更新時にTerraformの更新も必要になり、以下の問題点が発生します

* 開発側にTerraformの知見がなく、Terraformの更新が負担になる。開発側の実装速度低下
　* プロジェクトの体制上、開発側のキャッチアップを十分にとることは難しかった
* workbenchとTerraformファイルのダブルメンテナンスになる


そこで、exportしたworkbenchの定義をもとにTerraformでリソースを作成することにしました

## 実装方針

* AWS公式のTerraform DynamoDBで実装できることは可能にする
* itemまで実装
  * data modelにはデータも含めることが可能で、それも併せてデプロイできるようにします
* あくまでTerraform上は、１つのテーブルに１つのリソースを定義する
  * workbenchの定義ファイルには複数のテーブル情報が含まれるので不要にもできるが、テーブルを増減する場合は、Terraform側でも明示的に変更を入れる形にして、意図せずテーブルが増減するケースを防ぐ
  * ここは今後方針を変えてもよいかも


## 実装内容

以下抜粋。全コードはこちら

https://github.com/willco21/terrafrom-dynamodb-from-data-model


基本的にテーブルによって、使わない要素があるので、tryを使用

複数回定義される可能性がある要素は、Dynamic + for_eachを使用して、0からn個までの定義を可能にしました。


```tf
locals {
  range_key                = try(var.data_model.KeyAttributes.SortKey.AttributeName, null)
  read_capacity            = try(var.data_model.ProvisionedCapacitySettings.ProvisionedThroughput.ReadCapacityUnits, null)
  write_capacity           = try(var.data_model.ProvisionedCapacitySettings.ProvisionedThroughput.WriteCapacityUnits, null)
  auto_scaling_read        = try(var.data_model.ProvisionedCapacitySettings.AutoScalingRead, null)
  auto_scaling_write       = try(var.data_model.ProvisionedCapacitySettings.AutoScalingWrite, null)
  global_secondary_indexes = try(var.data_model.GlobalSecondaryIndexes, [])
  splat_table_data         = try(var.data_model.TableData, [])
  splat_table_facet_data   = try(var.data_model.TableFacets[*].TableData[0], [])
  table_data               = concat(local.splat_table_data, local.splat_table_facet_data)
}

resource "aws_dynamodb_table" "DynamoDBTable" {
  dynamic "attribute" {
    for_each = var.data_model.KeyAttributes
    content {
      name = attribute.value.AttributeName
      type = attribute.value.AttributeType
    }
  }
  dynamic "attribute" {
    for_each = { for i in local.global_secondary_indexes : i.IndexName => i.KeyAttributes.PartitionKey }
    iterator = gsi_partition_key
    content {
      name = gsi_partition_key.value.AttributeName
      type = gsi_partition_key.value.AttributeType
    }
  }
  dynamic "attribute" {
    for_each = { for i in local.global_secondary_indexes : i.IndexName => i.KeyAttributes.SortKey if can(i.KeyAttributes.SortKey) }
    iterator = gsi_sort_key
    content {
      name = gsi_sort_key.value.AttributeName
      type = gsi_sort_key.value.AttributeType
    }
  }
  name           = var.name
  hash_key       = var.data_model.KeyAttributes.PartitionKey.AttributeName
  range_key      = local.range_key
  billing_mode   = var.data_model.BillingMode
  read_capacity  = local.read_capacity
  write_capacity = local.write_capacity
  dynamic "global_secondary_index" {
    for_each = { for i in local.global_secondary_indexes : i.IndexName => i }
    content {
      name            = global_secondary_index.value.IndexName
      hash_key        = global_secondary_index.value.KeyAttributes.PartitionKey.AttributeName
      range_key       = try(global_secondary_index.value.KeyAttributes.SortKey.AttributeName, null)
      projection_type = global_secondary_index.value.Projection.ProjectionType
      read_capacity   = try(global_secondary_index.value.ProvisionedCapacitySettings.ProvisionedThroughput.ReadCapacityUnits, null)
      write_capacity  = try(global_secondary_index.value.ProvisionedCapacitySettings.ProvisionedThroughput.WriteCapacityUnits, null)
    }
  }
}

```

上記をモジュールとして定義し、それを以下のように使用します


```tf
locals {
  data_models_json     = file(var.datamodel_filepath)
  dynamodb_data_models = jsondecode(local.data_models_json).DataModel
}

data "aws_caller_identity" "current" {}


module "dynamodb_Forum" {
  ### Module Path
  source = "../../modules/DynamoDB/from_data_model"

  for_each = { for i in local.dynamodb_data_models : i.TableName => i if endswith(i.TableName, "Forum") }

  name       = "${var.resource_prefix}-Forum"
  data_model = each.value
}

module "dynamodb_Thread" {
  ### Module Path
  source = "../../modules/DynamoDB/from_data_model"

  for_each = { for i in local.dynamodb_data_models : i.TableName => i if endswith(i.TableName, "Thread") }

  name       = "${var.resource_prefix}-Thread"
  data_model = each.value
}

module "dynamodb_Reply" {
  ### Module Path
  source = "../../modules/DynamoDB/from_data_model"

  for_each = { for i in local.dynamodb_data_models : i.TableName => i if endswith(i.TableName, "Reply") }

  name       = "${var.resource_prefix}-Reply"
  data_model = each.value
}
```

## 振り返りと今後

NoSQL workbench自体は便利ですが、Terraformと絡めたときに管理上の煩雑さが出るので、そこを改善する１つのアプローチをしてみました。

（最も、Cfnを使えばそこも楽ではあるが。。それはIaCツールのポリシーによるかなと。このPJTではマルチクラウドだったのでTerraformを選択しています）

せっかく作成したのでモジュール化して公開をしたいと思います。