diff --git a/CHANGELOG.md b/CHANGELOG.md index 985aa5f..272956c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## [3.3.0](https://github.com/terraform-aws-modules/terraform-aws-dynamodb-table/compare/v3.2.0...v3.3.0) (2023-05-25) + + +### Features + +* Added option to ignore changes to GSIs ([#72](https://github.com/terraform-aws-modules/terraform-aws-dynamodb-table/issues/72)) ([44187e9](https://github.com/terraform-aws-modules/terraform-aws-dynamodb-table/commit/44187e9d56b6662eb6aef757d3322c92edc27d4b)) + ## [3.2.0](https://github.com/terraform-aws-modules/terraform-aws-dynamodb-table/compare/v3.1.2...v3.2.0) (2023-03-21) diff --git a/README.md b/README.md index 5676943..1210f74 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,13 @@ There are two separate Terraform resources used for the DynamoDB table: one is f terraform state mv module.dynamodb_table.aws_dynamodb_table.this module.dynamodb_table.aws_dynamodb_table.autoscaled ``` +**Warning: autoscaling with global secondary indexes** + +When using an autoscaled provisioned table with GSIs you may find that applying TF changes whilst a GSI is scaled up will reset the capacity, there +is an [open issue for this on the AWS Provider](https://github.com/hashicorp/terraform-provider-aws/issues/671). To get around this issue you can enable +the `ignore_changes_global_secondary_index` setting however, using this setting means that any changes to GSIs will be ignored by Terraform and will +hence have to be applied manually (or via some other automation). + ## Module wrappers Users of this Terraform module can create multiple similar resources by using [`for_each` meta-argument within `module` block](https://www.terraform.io/language/meta-arguments/for_each) which became available in Terraform 0.13. @@ -78,6 +85,7 @@ No modules. | [aws_appautoscaling_target.table_read](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/appautoscaling_target) | resource | | [aws_appautoscaling_target.table_write](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/appautoscaling_target) | resource | | [aws_dynamodb_table.autoscaled](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | +| [aws_dynamodb_table.autoscaled_gsi_ignore](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | | [aws_dynamodb_table.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | ## Inputs @@ -95,6 +103,7 @@ No modules. | [deletion\_protection\_enabled](#input\_deletion\_protection\_enabled) | Enables deletion protection for table | `bool` | `null` | no | | [global\_secondary\_indexes](#input\_global\_secondary\_indexes) | Describe a GSI for the table; subject to the normal limits on the number of GSIs, projected attributes, etc. | `any` | `[]` | no | | [hash\_key](#input\_hash\_key) | The attribute to use as the hash (partition) key. Must also be defined as an attribute | `string` | `null` | no | +| [ignore\_changes\_global\_secondary\_index](#input\_ignore\_changes\_global\_secondary\_index) | Whether to ignore changes lifecycle to global secondary indices, useful for provisioned tables with scaling | `bool` | `false` | no | | [import\_table](#input\_import\_table) | Configurations for importing s3 data into a new table. | `any` | `{}` | no | | [local\_secondary\_indexes](#input\_local\_secondary\_indexes) | Describe an LSI on the table; these can only be allocated at creation so you cannot change this definition after you have created the resource. | `any` | `[]` | no | | [name](#input\_name) | Name of the DynamoDB table | `string` | `null` | no | diff --git a/autoscaling.tf b/autoscaling.tf index 50e765e..30169bb 100644 --- a/autoscaling.tf +++ b/autoscaling.tf @@ -3,7 +3,7 @@ resource "aws_appautoscaling_target" "table_read" { max_capacity = var.autoscaling_read["max_capacity"] min_capacity = var.read_capacity - resource_id = "table/${aws_dynamodb_table.autoscaled[0].name}" + resource_id = "table/${try(aws_dynamodb_table.autoscaled[0].name, aws_dynamodb_table.autoscaled_gsi_ignore[0].name)}" scalable_dimension = "dynamodb:table:ReadCapacityUnits" service_namespace = "dynamodb" } @@ -33,7 +33,7 @@ resource "aws_appautoscaling_target" "table_write" { max_capacity = var.autoscaling_write["max_capacity"] min_capacity = var.write_capacity - resource_id = "table/${aws_dynamodb_table.autoscaled[0].name}" + resource_id = "table/${try(aws_dynamodb_table.autoscaled[0].name, aws_dynamodb_table.autoscaled_gsi_ignore[0].name)}" scalable_dimension = "dynamodb:table:WriteCapacityUnits" service_namespace = "dynamodb" } @@ -63,7 +63,7 @@ resource "aws_appautoscaling_target" "index_read" { max_capacity = each.value["read_max_capacity"] min_capacity = each.value["read_min_capacity"] - resource_id = "table/${aws_dynamodb_table.autoscaled[0].name}/index/${each.key}" + resource_id = "table/${try(aws_dynamodb_table.autoscaled[0].name, aws_dynamodb_table.autoscaled_gsi_ignore[0].name)}/index/${each.key}" scalable_dimension = "dynamodb:index:ReadCapacityUnits" service_namespace = "dynamodb" } @@ -93,7 +93,7 @@ resource "aws_appautoscaling_target" "index_write" { max_capacity = each.value["write_max_capacity"] min_capacity = each.value["write_min_capacity"] - resource_id = "table/${aws_dynamodb_table.autoscaled[0].name}/index/${each.key}" + resource_id = "table/${try(aws_dynamodb_table.autoscaled[0].name, aws_dynamodb_table.autoscaled_gsi_ignore[0].name)}/index/${each.key}" scalable_dimension = "dynamodb:index:WriteCapacityUnits" service_namespace = "dynamodb" } diff --git a/examples/autoscaling/main.tf b/examples/autoscaling/main.tf index 7a57c01..52dbc4e 100644 --- a/examples/autoscaling/main.tf +++ b/examples/autoscaling/main.tf @@ -9,13 +9,14 @@ resource "random_pet" "this" { module "dynamodb_table" { source = "../../" - name = "my-table-${random_pet.this.id}" - hash_key = "id" - range_key = "title" - billing_mode = "PROVISIONED" - read_capacity = 5 - write_capacity = 5 - autoscaling_enabled = true + name = "my-table-${random_pet.this.id}" + hash_key = "id" + range_key = "title" + billing_mode = "PROVISIONED" + read_capacity = 5 + write_capacity = 5 + autoscaling_enabled = true + ignore_changes_global_secondary_index = true autoscaling_read = { scale_in_cooldown = 50 diff --git a/main.tf b/main.tf index 3d04715..7ed6ee9 100644 --- a/main.tf +++ b/main.tf @@ -117,7 +117,7 @@ resource "aws_dynamodb_table" "this" { } resource "aws_dynamodb_table" "autoscaled" { - count = var.create_table && var.autoscaling_enabled ? 1 : 0 + count = var.create_table && var.autoscaling_enabled && !var.ignore_changes_global_secondary_index ? 1 : 0 name = var.name billing_mode = var.billing_mode @@ -237,3 +237,94 @@ resource "aws_dynamodb_table" "autoscaled" { ignore_changes = [read_capacity, write_capacity] } } + +resource "aws_dynamodb_table" "autoscaled_gsi_ignore" { + count = var.create_table && var.autoscaling_enabled && var.ignore_changes_global_secondary_index ? 1 : 0 + + name = var.name + billing_mode = var.billing_mode + hash_key = var.hash_key + range_key = var.range_key + read_capacity = var.read_capacity + write_capacity = var.write_capacity + stream_enabled = var.stream_enabled + stream_view_type = var.stream_view_type + table_class = var.table_class + deletion_protection_enabled = var.deletion_protection_enabled + + ttl { + enabled = var.ttl_enabled + attribute_name = var.ttl_attribute_name + } + + point_in_time_recovery { + enabled = var.point_in_time_recovery_enabled + } + + dynamic "attribute" { + for_each = var.attributes + + content { + name = attribute.value.name + type = attribute.value.type + } + } + + dynamic "local_secondary_index" { + for_each = var.local_secondary_indexes + + content { + name = local_secondary_index.value.name + range_key = local_secondary_index.value.range_key + projection_type = local_secondary_index.value.projection_type + non_key_attributes = lookup(local_secondary_index.value, "non_key_attributes", null) + } + } + + dynamic "global_secondary_index" { + for_each = var.global_secondary_indexes + + content { + name = global_secondary_index.value.name + hash_key = global_secondary_index.value.hash_key + projection_type = global_secondary_index.value.projection_type + range_key = lookup(global_secondary_index.value, "range_key", null) + read_capacity = lookup(global_secondary_index.value, "read_capacity", null) + write_capacity = lookup(global_secondary_index.value, "write_capacity", null) + non_key_attributes = lookup(global_secondary_index.value, "non_key_attributes", null) + } + } + + dynamic "replica" { + for_each = var.replica_regions + + content { + region_name = replica.value.region_name + kms_key_arn = lookup(replica.value, "kms_key_arn", null) + propagate_tags = lookup(replica.value, "propagate_tags", null) + point_in_time_recovery = lookup(replica.value, "point_in_time_recovery", null) + } + } + + server_side_encryption { + enabled = var.server_side_encryption_enabled + kms_key_arn = var.server_side_encryption_kms_key_arn + } + + tags = merge( + var.tags, + { + "Name" = format("%s", var.name) + }, + ) + + timeouts { + create = lookup(var.timeouts, "create", null) + delete = lookup(var.timeouts, "delete", null) + update = lookup(var.timeouts, "update", null) + } + + lifecycle { + ignore_changes = [global_secondary_index, read_capacity, write_capacity] + } +} diff --git a/outputs.tf b/outputs.tf index 310cfc5..fa07482 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,19 +1,19 @@ output "dynamodb_table_arn" { description = "ARN of the DynamoDB table" - value = try(aws_dynamodb_table.this[0].arn, aws_dynamodb_table.autoscaled[0].arn, "") + value = try(aws_dynamodb_table.this[0].arn, aws_dynamodb_table.autoscaled[0].arn, aws_dynamodb_table.autoscaled_gsi_ignore[0].arn, "") } output "dynamodb_table_id" { description = "ID of the DynamoDB table" - value = try(aws_dynamodb_table.this[0].id, aws_dynamodb_table.autoscaled[0].id, "") + value = try(aws_dynamodb_table.this[0].id, aws_dynamodb_table.autoscaled[0].id, aws_dynamodb_table.autoscaled_gsi_ignore[0].id, "") } output "dynamodb_table_stream_arn" { description = "The ARN of the Table Stream. Only available when var.stream_enabled is true" - value = var.stream_enabled ? try(aws_dynamodb_table.this[0].stream_arn, aws_dynamodb_table.autoscaled[0].stream_arn, "") : null + value = var.stream_enabled ? try(aws_dynamodb_table.this[0].stream_arn, aws_dynamodb_table.autoscaled[0].stream_arn, aws_dynamodb_table.autoscaled_gsi_ignore[0].stream_arn, "") : null } output "dynamodb_table_stream_label" { description = "A timestamp, in ISO 8601 format of the Table Stream. Only available when var.stream_enabled is true" - value = var.stream_enabled ? try(aws_dynamodb_table.this[0].stream_label, aws_dynamodb_table.autoscaled[0].stream_label, "") : null + value = var.stream_enabled ? try(aws_dynamodb_table.this[0].stream_label, aws_dynamodb_table.autoscaled[0].stream_label, aws_dynamodb_table.autoscaled_gsi_ignore[0].stream_label, "") : null } diff --git a/variables.tf b/variables.tf index 149c7b1..577ace5 100644 --- a/variables.tf +++ b/variables.tf @@ -173,3 +173,9 @@ variable "import_table" { type = any default = {} } + +variable "ignore_changes_global_secondary_index" { + description = "Whether to ignore changes lifecycle to global secondary indices, useful for provisioned tables with scaling" + type = bool + default = false +} diff --git a/wrappers/main.tf b/wrappers/main.tf index b6fe7ff..6ab374a 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -33,10 +33,11 @@ module "wrapper" { scale_out_cooldown = 0 target_value = 70 }) - autoscaling_read = try(each.value.autoscaling_read, var.defaults.autoscaling_read, {}) - autoscaling_write = try(each.value.autoscaling_write, var.defaults.autoscaling_write, {}) - autoscaling_indexes = try(each.value.autoscaling_indexes, var.defaults.autoscaling_indexes, {}) - table_class = try(each.value.table_class, var.defaults.table_class, null) - deletion_protection_enabled = try(each.value.deletion_protection_enabled, var.defaults.deletion_protection_enabled, null) - import_table = try(each.value.import_table, var.defaults.import_table, {}) + autoscaling_read = try(each.value.autoscaling_read, var.defaults.autoscaling_read, {}) + autoscaling_write = try(each.value.autoscaling_write, var.defaults.autoscaling_write, {}) + autoscaling_indexes = try(each.value.autoscaling_indexes, var.defaults.autoscaling_indexes, {}) + table_class = try(each.value.table_class, var.defaults.table_class, null) + deletion_protection_enabled = try(each.value.deletion_protection_enabled, var.defaults.deletion_protection_enabled, null) + import_table = try(each.value.import_table, var.defaults.import_table, {}) + ignore_changes_global_secondary_index = try(each.value.ignore_changes_global_secondary_index, var.defaults.ignore_changes_global_secondary_index, false) }