DynamoDB table
Some things people ask the terraform-aws-modules/dynamodb-table/aws module for
cannot be implemented by any module, in any registry: they are limits of
Terraform and OpenTofu themselves. This page is for engineers who hit one of
them. For each it says what causes it, gives the native workaround in full, and,
where one exists, shows the Operational Rule that removes the need for a fork.
Read the native option first; several are the right answer on their own.
DynamoDB read and write capacity reverts on every plan
Application Auto Scaling adjusts provisioned read and write capacity. Terraform holds the numbers from the configuration and resets them, which can throttle a table that had scaled up for a reason.
Why Terraform cannot fix this. Terraform's model is that it owns every attribute it can see. When a controller, an autoscaler, a scanner, or a deployment pipeline writes to one of those attributes, Terraform reads the new value as drift and plans to put its own value back. Both systems are behaving correctly; they simply disagree about who owns the field.
The only mechanism Terraform offers for splitting ownership is ignore_changes, and that is a lifecycle argument, which brings the literal-value restriction above with it. A module cannot accept the list of attributes to ignore as an input, so a module consumer has no way to declare the split.
| Repository | Issue | Title |
|---|---|---|
| hashicorp/terraform | #27360 | A method to override configuration and meta arguments within a module |
| hashicorp/terraform | #24188 | Support for dynamic blocks and meta-arguments |
| hashicorp/terraform-provider-aws | #19583 | Provider produced inconsistent final plan / an invalid new value for .tags_all |
The native workaround. Switch the table to on-demand billing (PAY_PER_REQUEST) where the workload suits it - there is then no provisioned capacity to disagree about.
For provisioned capacity, this module has a native option: set autoscaling_enabled = true. The module then creates the Application Auto Scaling targets and policies, and builds the table from a second resource block, aws_dynamodb_table.autoscaled, which ignores read_capacity and write_capacity. Turning it on for a table that already exists moves it to a new state address, so add a moved block from the module's aws_dynamodb_table.this[0] to aws_dynamodb_table.autoscaled[0] in the same change, or Terraform plans to replace it. The rule below ignores the capacity on the resource where it is, with no state move.
The rule that answers it. lifecycle_ignore_autoscaling_changes adds the
block for you, server-side, while the module is being downloaded. What arrives
in .terraform/modules/ is ordinary HCL with this change already in it, and
nothing else about the module changes: same inputs, same outputs, same version.
resource "aws_dynamodb_table" "this" {
# ... the module's own configuration, unchanged ...
+
+ lifecycle {
+ ignore_changes = [read_capacity, write_capacity]
+ }
}terraform destroy deleted a DynamoDB table
A table is destroyed by the same paths as any other resource, and a table holds application state that no plan review reliably catches on the way past.
Why Terraform cannot fix this. lifecycle is a meta-argument block, and Terraform evaluates it before it evaluates the rest of the configuration. Its arguments therefore cannot reference a variable, a local, or anything else that is computed. That is the whole reason no module can expose prevent_destroy, ignore_changes, or create_before_destroy as an input: there is no expression a module author could put there that Terraform would accept.
The request was first filed in 2015 (#3116, closed in 2023 without a change). The module-specific asks, #18367 and #22544, have been open since 2018 and 2019, and OpenTofu carries the same request. Until one of them ships a way to set a meta-argument from outside the resource block, every module in every registry has the same limitation.
| Repository | Issue | Title |
|---|---|---|
| hashicorp/terraform | #22544 | "Variables may not be used here" for prevent_destroy |
| hashicorp/terraform | #18367 | Feature request: support prevent_destroy for modules |
| hashicorp/terraform | #24746 | Ability to use locals or wildcards in ignore_changes lifecycle block |
| hashicorp/terraform | #24188 | Support for dynamic blocks and meta-arguments |
| hashicorp/terraform | #27360 | A method to override configuration and meta arguments within a module |
| opentofu/opentofu | #1329 | Support variables in lifecycle blocks |
The native workaround. Set deletion_protection_enabled = true, which this module exposes and which AWS enforces on its side, and turn on point-in-time recovery so a deletion is recoverable. For a stop at plan time, the lifecycle block has to be on the table resource. Fork this module and add the lifecycle block to the resource yourself. That works, and module maintainers do it when they need it. The cost is ongoing rather than one-off: the fork has to be re-synced with every upstream release, and each sync needs a review to confirm the block still lands on the resource it was meant for. A wrapper module does not avoid this, because the lifecycle block still has to sit inside the resource block, which is inside the module you did not write.
The rule that answers it. lifecycle_prevent_destroy_data adds the
block for you, server-side, while the module is being downloaded. What arrives
in .terraform/modules/ is ordinary HCL with this change already in it, and
nothing else about the module changes: same inputs, same outputs, same version.
resource "aws_dynamodb_table" "this" {
# ... the module's own configuration, unchanged ...
+
+ lifecycle {
+ prevent_destroy = true
+ }
}Use the rule
This source line applies the rules above during terraform init. The inputs
and outputs stay the upstream module's.
module "dynamodb_table" {
source = "https://registry.compliance.tf/terraform-aws-modules/dynamodb-table/aws?version=x.y.z&add_rules=lifecycle_ignore_autoscaling_changes,lifecycle_prevent_destroy_data"
# ... the same inputs you pass today; the version is in the URL above ...
}- Pin the version in the URL.
?version=takes an exact release; without it the registry serves the latest. Delete theversionargument, which Terraform accepts only on registry-protocol sources. Several rules go in?add_rules=, separated by commas. - Keep the token out of the source line. Register a free compliance.tf
account and configure an access token; it never
goes in the
sourceline or in git. - Preview in the Playground, within its limits. The
Rules Playground shows the diff with no account, at
the version it pins for DynamoDB table, which need not be yours. It reads the
module root only: a rule whose resource lives in a submodule shows no diff
there even though the download applies it, and a module that publishes
nothing but submodules cannot be previewed. It takes the list as
?rules=because it previews rules on their own rather than against your Baseline.
On registry.compliance.tf, or on a framework host, ?add_rules= adds to
whatever Baseline your organization has Enforced; on an organization host that
organization's configuration decides. A module compliance.tf builds for you
carries a compliancetf-manifest.json naming every rule the build received,
each with an outcome recording what it did.
Related
- Rules overview - what rules are, and how they differ from compliance controls
- Rule definitions - the HCL of every selectable rule
- lifecycle_ignore_autoscaling_changes - the rule page, with limits and failure modes
- lifecycle_prevent_destroy_data - the rule page, with limits and failure modes
- Stop paying the Terraform fork tax - why forking a module to add a
lifecycleblock costs more than it looks - DynamoDB table module reference - inputs, outputs, and the controls applied to it