RDS: known Terraform limitations
Some things people ask the terraform-aws-modules/rds/aws module for
cannot be implemented by any module, in any registry. They are limits of
Terraform itself. This page collects the recurring ones for this module, says
plainly what causes each, gives the native workaround in full, and - where one
exists - shows the Operational Rule that removes the need for a fork.
This module is downloaded ~1.5M times a month, counted together with rds-aurora, so these come up often.
Read the native option first
Every problem below names the workaround you can apply today without compliance.tf. Several of them are the right answer on their own. The rule is an alternative to maintaining a fork, not a replacement for a control AWS already offers you.
terraform destroy deleted a production database
The same replacement and wrong-workspace paths that delete a bucket delete a database instance, and the blast radius is larger. prevent_destroy is the built-in guard, and it cannot be passed into this module.
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.
This is a property of Terraform, not of this module. The request to lift the restriction has been open since 2015, it is one of the most-supported requests in the tracker, 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 | #3116 | Cannot use interpolations in lifecycle attributes |
| hashicorp/terraform | #18367 | Feature request: support prevent_destroy for modules |
| hashicorp/terraform | #21546 | Passing ignore_changes into a module |
| 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 = true, which this module does expose as an input and which AWS enforces on its side. That is the first thing to do and it is not a workaround - it is the right control for this resource.
It is not the same guarantee as prevent_destroy, though: deletion protection can itself be turned off by the same apply that then deletes the instance, and it does not cover a replacement Terraform performs for another reason. For that, the lifecycle block has to be on the resource. Fork this module and add the lifecycle block to the resource yourself. That works, and it is the honest answer - it is what module maintainers do 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:
resource "aws_db_instance" "this" {
# ... the module's own configuration, unchanged ...
+
+ lifecycle {
+ prevent_destroy = true
+ }
}Nothing else about the module changes: same inputs, same outputs, same version. The full definition is published at lifecycle_prevent_destroy_data.
Terraform wants to reset the database password on every plan
Secrets Manager rotation, or an operator rotating the master password out of band, changes a value Terraform believes it owns. Every plan afterwards wants to set the password back to the one in state, and applying it reverts the rotation.
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
Stop managing the password in Terraform. manage_master_user_password hands rotation to AWS entirely and removes the disagreement, and this module exposes it. Where the password has to stay in Terraform, the native option is ignore_changes = [password] on the instance, which is a lifecycle argument. Fork this module and add the lifecycle block to the resource yourself. That works, and it is the honest answer - it is what module maintainers do 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.
Managed fix
Native option only, today. compliance.tf has no rule for this yet, so the workaround above is the whole answer. A rule that ignores rotated credentials is not built yet.
Using the maintained alternative
compliance.tf serves this module from a registry that applies the rules above
during terraform init. The change is the source line plus the rules you
want; the inputs and outputs are the upstream module's.
module "rds" {
source = "https://cis.compliance.tf/terraform-aws-modules/rds/aws?add_rules=lifecycle_prevent_destroy_data"
# ... the same inputs you pass today ...
}On a framework host you name the rules you want in ?add_rules=, on top of 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 actually did, so the change is auditable
rather than implicit.
Related
- Operational Rules - what rules are, and how they differ from compliance controls
- Operational Rule Definitions - the HCL of every selectable rule
- 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 - RDS module reference - inputs, outputs, and the controls applied to it