From e4e65a80a8ac8ca309a7357e3a0a3df3376e1d9d Mon Sep 17 00:00:00 2001 From: Nikolaos Fideropoulos Date: Wed, 3 Dec 2025 18:18:19 +0100 Subject: [PATCH] initial set of changes --- README.md | 10 +++ main.tf | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++- variables.tf | 6 ++ 3 files changed, 214 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e889485..3362816 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,16 @@ need to move the old `aws_dynamodb_table` resource that is being `destroyed` to terraform state mv module.dynamodb_table.aws_dynamodb_table.autoscaled module.dynamodb_table.aws_dynamodb_table.autoscaled_ignore_gsi ``` +**Warning: AWS-managed warm throughput drift** + +When using PAY_PER_REQUEST billing mode, AWS automatically adjusts warm throughput values for tables and GSIs based on usage patterns at no additional cost. This causes Terraform to detect drift even though no intentional changes were made. To ignore these AWS-managed adjustments while still detecting intentional configuration changes, you can enable the `ignore_warm_throughput_changes` setting. + +**NOTE**: Setting `ignore_warm_throughput_changes` after the table is already created causes your table to be recreated. In this case, you will need to move the old `aws_dynamodb_table` resource that is being `destroyed` to the new resource that is being `created`. For example: + +``` +terraform state mv module.dynamodb_table.aws_dynamodb_table.this module.dynamodb_table.aws_dynamodb_table.warm_throughput_ignore +``` + ## 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. diff --git a/main.tf b/main.tf index 02340f1..9d86804 100644 --- a/main.tf +++ b/main.tf @@ -1,9 +1,9 @@ locals { - dynamodb_table_arn = try(aws_dynamodb_table.this[0].arn, aws_dynamodb_table.autoscaled[0].arn, aws_dynamodb_table.autoscaled_gsi_ignore[0].arn, "") + dynamodb_table_arn = try(aws_dynamodb_table.this[0].arn, aws_dynamodb_table.autoscaled[0].arn, aws_dynamodb_table.autoscaled_gsi_ignore[0].arn, aws_dynamodb_table.warm_throughput_ignore[0].arn, "") } resource "aws_dynamodb_table" "this" { - count = var.create_table && !var.autoscaling_enabled ? 1 : 0 + count = var.create_table && !var.autoscaling_enabled && !var.ignore_warm_throughput_changes ? 1 : 0 name = var.name billing_mode = var.billing_mode @@ -435,6 +435,202 @@ resource "aws_dynamodb_table" "autoscaled_gsi_ignore" { } } +resource "aws_dynamodb_table" "warm_throughput_ignore" { + count = var.create_table && !var.autoscaling_enabled && var.ignore_warm_throughput_changes ? 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 + region = var.region + restore_date_time = var.restore_date_time + restore_source_name = var.restore_source_name + restore_source_table_arn = var.restore_source_table_arn + restore_to_latest_time = var.restore_to_latest_time + + ttl { + enabled = var.ttl_enabled + attribute_name = var.ttl_attribute_name + } + + point_in_time_recovery { + enabled = var.point_in_time_recovery_enabled + recovery_period_in_days = var.point_in_time_recovery_period_in_days + } + + 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 "on_demand_throughput" { + for_each = try([global_secondary_index.value.on_demand_throughput], []) + + content { + max_read_request_units = try(on_demand_throughput.value.max_read_request_units, null) + max_write_request_units = try(on_demand_throughput.value.max_write_request_units, null) + } + } + + dynamic "warm_throughput" { + for_each = try([global_secondary_index.value.warm_throughput], []) + + content { + read_units_per_second = try(warm_throughput.value.read_units_per_second, null) + write_units_per_second = try(warm_throughput.value.write_units_per_second, 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) + deletion_protection_enabled = lookup(replica.value, "deletion_protection_enabled", null) + consistency_mode = try(replica.value.consistency_mode, null) + } + } + + server_side_encryption { + enabled = var.server_side_encryption_enabled + kms_key_arn = var.server_side_encryption_kms_key_arn + } + + dynamic "import_table" { + for_each = length(var.import_table) > 0 ? [var.import_table] : [] + + content { + input_format = import_table.value.input_format + input_compression_type = try(import_table.value.input_compression_type, null) + + dynamic "input_format_options" { + for_each = try([import_table.value.input_format_options], []) + + content { + + dynamic "csv" { + for_each = try([input_format_options.value.csv], []) + + content { + delimiter = try(csv.value.delimiter, null) + header_list = try(csv.value.header_list, null) + } + } + } + } + + s3_bucket_source { + bucket = import_table.value.bucket + bucket_owner = try(import_table.value.bucket_owner, null) + key_prefix = try(import_table.value.key_prefix, null) + } + } + } + + dynamic "on_demand_throughput" { + for_each = length(var.on_demand_throughput) > 0 ? [var.on_demand_throughput] : [] + + content { + max_read_request_units = try(on_demand_throughput.value.max_read_request_units, null) + max_write_request_units = try(on_demand_throughput.value.max_write_request_units, null) + } + } + + dynamic "warm_throughput" { + for_each = length(var.warm_throughput) > 0 ? [var.warm_throughput] : [] + + content { + read_units_per_second = try(warm_throughput.value.read_units_per_second, null) + write_units_per_second = try(warm_throughput.value.write_units_per_second, null) + } + } + + dynamic "global_table_witness" { + for_each = var.global_table_witness != null ? [var.global_table_witness] : [] + + content { + region_name = global_table_witness.value.region_name + } + } + + 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[0].warm_throughput, + global_secondary_index[1].warm_throughput, + global_secondary_index[2].warm_throughput, + global_secondary_index[3].warm_throughput, + global_secondary_index[4].warm_throughput, + global_secondary_index[5].warm_throughput, + global_secondary_index[6].warm_throughput, + global_secondary_index[7].warm_throughput, + global_secondary_index[8].warm_throughput, + global_secondary_index[9].warm_throughput, + global_secondary_index[10].warm_throughput, + global_secondary_index[11].warm_throughput, + global_secondary_index[12].warm_throughput, + global_secondary_index[13].warm_throughput, + global_secondary_index[14].warm_throughput, + global_secondary_index[15].warm_throughput, + global_secondary_index[16].warm_throughput, + global_secondary_index[17].warm_throughput, + global_secondary_index[18].warm_throughput, + global_secondary_index[19].warm_throughput, + warm_throughput + ] + } +} + resource "aws_dynamodb_resource_policy" "this" { count = var.create_table && var.resource_policy != null ? 1 : 0 diff --git a/variables.tf b/variables.tf index 6fc67f2..09fda1a 100644 --- a/variables.tf +++ b/variables.tf @@ -186,6 +186,12 @@ variable "ignore_changes_global_secondary_index" { default = false } +variable "ignore_warm_throughput_changes" { + description = "Whether to ignore changes to warm_throughput in global secondary indices and table-level warm_throughput. Useful when AWS automatically adjusts these values based on usage patterns in PAY_PER_REQUEST mode" + type = bool + default = false +} + variable "on_demand_throughput" { description = "Sets the maximum number of read and write units for the specified on-demand table" type = any