TL;DR
- NoSQL workbenchは作成したdata modelをexportできる
- exportした定義をもとにTerraformでリソースを作成したい
- 既存の仕組みではできなそうなので、自分で作りました
NoSQL workbenchとは
NoSQL Workbench は、DynamoDB テーブルの設計、作成、クエリ、管理に役立つデータモデリング、データ可視化、クエリ開発といった特徴を提供する視覚的開発ツールです。
要するに、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上は、1つのテーブルに1つのリソースを定義する
- workbenchの定義ファイルには複数のテーブル情報が含まれるので不要にもできるが、テーブルを増減する場合は、Terraform側でも明示的に変更を入れる形にして、意図せずテーブルが増減するケースを防ぐ
- ここは今後方針を変えてもよいかも
実装内容
以下抜粋。全コードはこちら
基本的にテーブルによって、使わない要素があるので、tryを使用
複数回定義される可能性がある要素は、Dynamic + for_eachを使用して、0からn個までの定義を可能にしました。
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)
}
}
}
上記をモジュールとして定義し、それを以下のように使用します
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と絡めたときに管理上の煩雑さが出るので、そこを改善する1つのアプローチをしてみました。
(最も、Cfnを使えばそこも楽ではあるが。。それはIaCツールのポリシーによるかなと。このPJTではマルチクラウドだったのでTerraformを選択しています)
せっかく作成したのでモジュール化して公開をしたいと思います。