compliance.tf

Set Deletion Protection

Sets the provider's deletion-protection attribute to true on RDS instances, Aurora clusters, DynamoDB tables and load balancers.

When to use this rule

Use this when: Your modules declare databases, tables or load balancers without setting deletion protection at all, and you want the protection enforced by AWS rather than only by Terraform.

Do not use this when: You run ephemeral environments that are destroyed after each run. Destroying a protected resource takes two applies: one to turn the attribute off, one to destroy.


Why this rule exists

These services offer deletion protection as an attribute AWS itself enforces, and the upstream modules leave it unset unless every caller remembers the input. Once set, a terraform destroy, a console click and an API call are all refused until someone turns it off first.

It is a stronger guard than prevent_destroy, which only Terraform honours, and it can be set on the resource without a fork only if the module exposes the input and the caller sets it.

RepositoryIssueTitle
hashicorp/terraform#22544"Variables may not be used here" for prevent_destroy
hashicorp/terraform#18367Feature request: support prevent_destroy for modules
hashicorp/terraform#24746Ability to use locals or wildcards in ignore_changes lifecycle block
hashicorp/terraform#24188Support for dynamic blocks and meta-arguments
hashicorp/terraform#27360A method to override configuration and meta arguments within a module
opentofu/opentofu#1329Support variables in lifecycle blocks

Affected resources

ResourceServiceWhy
aws_db_instanceAmazon RDSdeletion_protection, left unset by default
aws_rds_clusterAmazon Auroradeletion_protection, left unset by default
aws_dynamodb_tableAmazon DynamoDBdeletion_protection_enabled, left unset by default
aws_lbElastic Load Balancingenable_deletion_protection, left unset by default

What this rule does

Sets a provider attribute on each matching resource, one attribute name per resource type:

ResourceAttribute set
aws_db_instancedeletion_protection = true
aws_rds_clusterdeletion_protection = true
aws_dynamodb_tabledeletion_protection_enabled = true
aws_lbenable_deletion_protection = true

A resource that leaves the attribute unset gains it. A resource that already sets it, to a literal or to a module variable, keeps what it has: an explicit value in the module wins, and the rule reports not_applicable for it. Unlike a lifecycle argument, this is a provider attribute, so once set the protection lives in AWS and applies to every client, not only to Terraform.


Before and after

Before (upstream module):

resource "aws_db_instance" "this" {
  # ... resource configuration ...

  tags = var.tags
}

After (with Set Deletion Protection applied):

resource "aws_db_instance" "this" {
  # ... resource configuration ...

  deletion_protection = true

  tags = var.tags
}

The attribute name differs per resource type (see "What this rule does" above); the shape of the change is the same for each.

The only change is the rule transformation. All existing arguments, outputs, and module behavior remain identical.

Real-world scenario

A cleanup script deleted a load balancer by ARN, straight through the API. Deletion protection would have refused the call regardless of what Terraform knew.


Known limits

  • The four resource types are fixed for this rule and cannot be retargeted by a parameter.
  • Only a resource that leaves the attribute unset is changed. The upstream terraform-aws-modules for RDS, Aurora and DynamoDB already expose it as a module input (deletion_protection, deletion_protection_enabled), and an explicit value in the module wins, so on those modules the rule reports not_applicable and the input is the place to set it.
  • Sets the attribute only. It does not add prevent_destroy; use lifecycle_prevent_destroy_data for the Terraform-side guard as well.
  • Does not protect the data inside the resource, only the resource itself.
  • Only .tf and .tofu files are read. Modules written in JSON syntax (.tf.json, .tofu.json) are left untouched.
  • Only the module's own directory and its first-level modules/* submodules are processed. Modules that this module calls by source URL are separate downloads and are handled on their own request.

The rule's definition

The HCL this rule is written in - its parameters, their shipped defaults, and the transformers it runs - is published at resource_set_deletion_protection, alongside every other selectable rule.


How to enable

On a framework host, per request:

Add ?rules=resource_set_deletion_protection to your HTTPS module source, using the id exactly as written.

module "example" {
  source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&rules=resource_set_deletion_protection"
}

A ?rules= list is either all bare names, which replace your organization's rules for that download, or all - prefixed names, which subtract from them. Mixing the two is refused with a 400 rather than silently treated as a replacement. To add this rule on top of your organization's rules instead, use ?add_rules=resource_set_deletion_protection; a bare ?rules= list cannot be combined with it, because a bare list already replaced the set there was something to add to.

The + add prefix was removed. It reached the registry only when written %2B, and a literal + in a query string decodes to a space everywhere, so ?rules=+resource_set_deletion_protection arrived as a bare name with a leading space — one step from silently replacing your organization's rules. Any +, in either spelling, is now a 400 naming ?add_rules=.

On your organization's host:

Add the rule to the organization's Baseline, which applies to every module served from the bare organization form <alias>.compliance.tf/<namespace>/<name>/<provider>, or to a named ruleset that one module selects with ?ruleset=<name> on the HTTPS form of that address. A project environment (<alias>.compliance.tf/<project>/<env>/<module>) serves the rules frozen in its bound snapshot instead: there the rule goes into the config draft and reaches the environment by promotion. On both organization forms ?rules= and ?add_rules= are refused with a 400 — including the subtractive - prefix — rather than dropped, so you cannot mistake an ignored override for an applied one. Change the configuration, not the URL. See Baseline rules and rulesets for the configuration flow, and registry resolution for how the address forms differ.


Break-glass path

On a framework host, add ?rules=-resource_set_deletion_protection to the module source URL and run terraform init -upgrade to fetch a copy without this rule. The copy in .terraform/modules/ stays until the next upgrade, so remove the override when you are done. On your organization's host the parameter is refused with a 400 rather than ignored. The rule comes from the organization's configuration: a Baseline rule applies to every module served from the bare organization form, so an admin disables it for the duration of the operation, or keeps it out of the Baseline and in a ruleset that only the modules needing it select. On a project environment the posture is the bound snapshot and its waivers. See Baseline rules and rulesets.


Failure modes

ScenarioResult
Rule targets a module with no matching resource typesNo-op. Module is delivered unchanged and the manifest records not_applicable.
The module already sets the attribute, from a literal or a variableLeft as is and recorded as not_applicable. Set the module's own input instead.
Someone runs terraform destroy on a protected resourceAWS refuses the deletion and Terraform reports the API error. Set the attribute to false, apply, then destroy.

Terraform and OpenTofu compatible

This rule works with both Terraform (1.x+) and OpenTofu (1.6+). It reads ordinary HCL, and anything it changes stays ordinary HCL, so the module behaves the same under either tool.

Help us improve this page

Operational Rules are a new feature. We'd love your feedback on this rule page — what's useful, what's missing, what's confusing. Share feedback.

On this page

Ask AI about this

Help improve this page